@nudojs/cli 0.2.0 → 0.2.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/CHANGELOG.md +12 -0
- package/README.md +34 -0
- package/dist/chunk-3HCPXPEW.js +5286 -0
- package/dist/evaluator-api.d.ts +94 -2
- package/dist/evaluator-api.js +187 -3
- package/dist/index.js +595 -130
- package/examples/real-world.js +66 -0
- package/examples/validate-real-world.ts +160 -0
- package/package.json +17 -2
- package/src/__tests__/builtin-advanced.test.ts +292 -0
- package/src/__tests__/builtin-api.test.ts +308 -0
- package/src/__tests__/builtin-functions.test.ts +313 -0
- package/src/__tests__/builtin-map.test.ts +129 -0
- package/src/__tests__/builtin-promise.test.ts +150 -0
- package/src/__tests__/builtin-regexp.test.ts +133 -0
- package/src/__tests__/builtin-set.test.ts +163 -0
- package/src/__tests__/builtin-url.test.ts +256 -0
- package/src/__tests__/builtin-web.test.ts +340 -0
- package/src/__tests__/combination-scenarios.test.ts +384 -0
- package/src/__tests__/edge-cases.test.ts +749 -0
- package/src/__tests__/evaluator-loops.test.ts +120 -0
- package/src/__tests__/evaluator-nullish-coalescing.test.ts +86 -0
- package/src/__tests__/evaluator-optional-chaining.test.ts +37 -0
- package/src/__tests__/evaluator-phase3.test.ts +353 -2
- package/src/__tests__/evaluator.test.ts +702 -2
- package/src/__tests__/mock-arrow-function.test.ts +169 -0
- package/src/__tests__/mock-comparison.test.ts +221 -0
- package/src/__tests__/mock-comprehensive.test.ts +250 -0
- package/src/__tests__/mock-final-integration.test.ts +263 -0
- package/src/__tests__/mock-helpers.test.ts +174 -0
- package/src/__tests__/mock-realworld.test.ts +191 -0
- package/src/__tests__/mock-rest-debug.test.ts +110 -0
- package/src/__tests__/mock-rest-edge.test.ts +176 -0
- package/src/__tests__/mock-sinon-runtime.test.ts +168 -0
- package/src/__tests__/narrowing-discriminated.test.ts +35 -0
- package/src/__tests__/narrowing-in-operator.test.ts +56 -0
- package/src/__tests__/narrowing-switch.test.ts +56 -0
- package/src/__tests__/narrowing-truthiness.test.ts +52 -0
- package/src/__tests__/nudo-mock-syntax.test.ts +245 -0
- package/src/__tests__/realworld-patterns.test.ts +430 -0
- package/src/__tests__/syntax-sugar.test.ts +345 -0
- package/src/builtins/builtin-intl.ts +17 -0
- package/src/builtins/builtin-map.ts +174 -0
- package/src/builtins/builtin-promise.ts +116 -0
- package/src/builtins/builtin-reflect.ts +30 -0
- package/src/builtins/builtin-regexp.ts +34 -0
- package/src/builtins/builtin-set.ts +131 -0
- package/src/builtins/builtin-symbol.ts +16 -0
- package/src/builtins/builtin-url.ts +58 -0
- package/src/builtins/builtin-weak.ts +22 -0
- package/src/builtins/builtin-web.ts +111 -0
- package/src/builtins/index.ts +61 -0
- package/src/config.ts +32 -0
- package/src/env-loader.ts +174 -0
- package/src/evaluator-api.ts +20 -0
- package/src/evaluator.ts +4018 -342
- package/src/index.ts +776 -137
- package/src/narrowing.ts +230 -15
- package/src/resolve-npm.ts +63 -0
- package/dist/chunk-LGWVGEMB.js +0 -1817
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
import { describe, it, expect } from "vitest";
|
|
2
|
+
import { parse, extractDirectives, type CaseDirective, parseTypeValueExpr } from "@nudojs/parser";
|
|
3
|
+
import { typeValueToString, createEnvironment, T } from "@nudojs/core";
|
|
4
|
+
import { evaluateFunctionFull } from "../evaluator.js";
|
|
5
|
+
import { analyzeFile } from "@nudojs/service";
|
|
6
|
+
|
|
7
|
+
function inferWithMocks(source: string): { name: string; caseName: string; result: string }[] {
|
|
8
|
+
const ast = parse(source);
|
|
9
|
+
const directives = extractDirectives(ast);
|
|
10
|
+
const env = createEnvironment();
|
|
11
|
+
const results: { name: string; caseName: string; result: string }[] = [];
|
|
12
|
+
|
|
13
|
+
for (const fn of directives) {
|
|
14
|
+
// Apply mocks first
|
|
15
|
+
for (const d of fn.directives) {
|
|
16
|
+
if (d.kind === "mock") {
|
|
17
|
+
if (d.arrowFn) {
|
|
18
|
+
const fnType = T.fn(d.arrowFn.params, d.arrowFn.body, env);
|
|
19
|
+
(fnType as any)._paramPatterns = d.arrowFn.paramPatterns;
|
|
20
|
+
env.bind(d.name, fnType);
|
|
21
|
+
} else if (d.expression) {
|
|
22
|
+
env.bind(d.name, parseTypeValueExpr(d.expression));
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const caseDirectives = fn.directives.filter((d): d is CaseDirective => d.kind === "case");
|
|
28
|
+
for (const dir of caseDirectives) {
|
|
29
|
+
const result = evaluateFunctionFull(fn.node, dir.args, env);
|
|
30
|
+
results.push({
|
|
31
|
+
name: fn.name,
|
|
32
|
+
caseName: dir.name,
|
|
33
|
+
result: typeValueToString(result.value),
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
return results;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
describe("Mock Arrow Functions", () => {
|
|
41
|
+
it("supports arrow function mock with direct call", () => {
|
|
42
|
+
const results = inferWithMocks(`
|
|
43
|
+
// @nudo:mock fn = (x) => x > 3
|
|
44
|
+
// @nudo:case "num" (5)
|
|
45
|
+
function test(x) {
|
|
46
|
+
return fn(x);
|
|
47
|
+
}
|
|
48
|
+
`);
|
|
49
|
+
// fn(5) should return true since 5 > 3
|
|
50
|
+
expect(results[0].result).toBe("true");
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
it("supports arrow function mock for filter", () => {
|
|
54
|
+
const results = inferWithMocks(`
|
|
55
|
+
// @nudo:mock fn = (x) => x > 3
|
|
56
|
+
// @nudo:case "nums" ([1, 2, 3, 4, 5])
|
|
57
|
+
function filter(arr) {
|
|
58
|
+
const result = [];
|
|
59
|
+
for (const item of arr) {
|
|
60
|
+
if (fn(item)) result.push(item);
|
|
61
|
+
}
|
|
62
|
+
return result;
|
|
63
|
+
}
|
|
64
|
+
`);
|
|
65
|
+
// Should filter to [4, 5]
|
|
66
|
+
expect(results[0].result).toBe("[4, 5]");
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
it("supports arrow function mock with multiple params", () => {
|
|
70
|
+
const results = inferWithMocks(`
|
|
71
|
+
// @nudo:mock fn = (a, b) => a + b
|
|
72
|
+
// @nudo:case "nums" ([1, 2, 3])
|
|
73
|
+
function sum(arr) {
|
|
74
|
+
return arr.reduce(fn, 0);
|
|
75
|
+
}
|
|
76
|
+
`);
|
|
77
|
+
// reduce with (a, b) => a + b and [1, 2, 3] should give 6
|
|
78
|
+
expect(results[0].result).toBe("6");
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
it("supports arrow function mock with string operations", () => {
|
|
82
|
+
const results = inferWithMocks(`
|
|
83
|
+
// @nudo:mock fn = (x) => x.toUpperCase()
|
|
84
|
+
// @nudo:case "str" ("hello")
|
|
85
|
+
function transform(x) {
|
|
86
|
+
return fn(x);
|
|
87
|
+
}
|
|
88
|
+
`);
|
|
89
|
+
// fn("hello") should return "HELLO"
|
|
90
|
+
expect(results[0].result).toBe('"HELLO"');
|
|
91
|
+
});
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
describe("Mock Arrow Functions: builtin HOF propagation", () => {
|
|
95
|
+
it("mock arrow propagates through items.map(fn) with object results", () => {
|
|
96
|
+
const results = inferWithMocks(`
|
|
97
|
+
// @nudo:mock fn = (x) => ({ ok: true })
|
|
98
|
+
// @nudo:case "items" ([1, 2, 3])
|
|
99
|
+
function applyAll(items) {
|
|
100
|
+
return items.map(fn);
|
|
101
|
+
}
|
|
102
|
+
`);
|
|
103
|
+
// mock 函数值透传给 builtin HOF:与内联箭头 items.map(x => ({ ok: true })) 同机制
|
|
104
|
+
expect(results[0].result).toBe("[{ ok: true }, { ok: true }, { ok: true }]");
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
it("mock arrow keeps propagating when the receiver is a union of arrays", () => {
|
|
108
|
+
const results = inferWithMocks(`
|
|
109
|
+
// @nudo:mock fn = (x) => ({ ok: true })
|
|
110
|
+
// @nudo:case "go" ()
|
|
111
|
+
function applyUnion() {
|
|
112
|
+
const items = Math.random() > 0.5 ? [1, 2] : [3, 4, 5];
|
|
113
|
+
return items.map(fn);
|
|
114
|
+
}
|
|
115
|
+
`);
|
|
116
|
+
// union 接收者按成员分布求值,不再保守降级 unknown
|
|
117
|
+
expect(results[0].result).toBe("[{ ok: true }, { ok: true }] | [{ ok: true }, { ok: true }, { ok: true }]");
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
it("mock arrow reduce(fn, 0) over a union receiver still accumulates", () => {
|
|
121
|
+
const results = inferWithMocks(`
|
|
122
|
+
// @nudo:mock add = (a, b) => a + b
|
|
123
|
+
// @nudo:case "go" ()
|
|
124
|
+
function sumUnion() {
|
|
125
|
+
const xs = Math.random() > 0.5 ? [1, 2] : [3, 4, 5];
|
|
126
|
+
return xs.reduce(add, 0);
|
|
127
|
+
}
|
|
128
|
+
`);
|
|
129
|
+
expect(results[0].result).toBe("3 | 12");
|
|
130
|
+
});
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
describe("Mock Arrow Functions: callback arg through call sites", () => {
|
|
134
|
+
// 直接走 analyzer 主路径:mock 绑定必须先于全程序求值,顶层调用点
|
|
135
|
+
// (mockHof([1,2,3]))在 evaluateProgram 中求值并记录 call@ case——
|
|
136
|
+
// mock 晚于该点绑定会让「mock 函数值作为回调实参」整体降级 unknown。
|
|
137
|
+
it("top-level call site: items.map(fn) with a mock arrow yields precise elements", () => {
|
|
138
|
+
const result = analyzeFile("mock-hof-callsite.js", `
|
|
139
|
+
// @nudo:mock fn = (x) => ({ ok: true, v: x })
|
|
140
|
+
function mockHof(items) { return items.map(fn); }
|
|
141
|
+
mockHof([1, 2, 3]);
|
|
142
|
+
`);
|
|
143
|
+
const fn = result.functions.find((f) => f.name === "mockHof");
|
|
144
|
+
expect(fn).toBeDefined();
|
|
145
|
+
expect(fn!.cases[0].source).toBe("callsite");
|
|
146
|
+
expect(typeValueToString(fn!.cases[0].result)).toBe("[{ ok: true, v: 1 }, { ok: true, v: 2 }, { ok: true, v: 3 }]");
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
it("in-function call site: mock arrow through map/filter/reduce stays precise", () => {
|
|
150
|
+
const result = analyzeFile("mock-hof-inner.js", `
|
|
151
|
+
// @nudo:mock fn = (x) => ({ ok: true, v: x })
|
|
152
|
+
// @nudo:mock pred = (x) => x > 1
|
|
153
|
+
// @nudo:mock add = (a, b) => a + b
|
|
154
|
+
function innerMap(items) { return items.map(fn); }
|
|
155
|
+
function innerFilter(xs) { return xs.filter(pred); }
|
|
156
|
+
function innerReduce(xs) { return xs.reduce(add, 0); }
|
|
157
|
+
const a = innerMap([1, 2]);
|
|
158
|
+
const b = innerFilter([1, 2, 3]);
|
|
159
|
+
const c = innerReduce([1, 2, 3]);
|
|
160
|
+
`);
|
|
161
|
+
const map = result.functions.find((f) => f.name === "innerMap");
|
|
162
|
+
const filter = result.functions.find((f) => f.name === "innerFilter");
|
|
163
|
+
const reduce = result.functions.find((f) => f.name === "innerReduce");
|
|
164
|
+
expect(typeValueToString(map!.cases[0].result)).toBe("[{ ok: true, v: 1 }, { ok: true, v: 2 }]");
|
|
165
|
+
// filter 不丢元素类型
|
|
166
|
+
expect(typeValueToString(filter!.cases[0].result)).toBe("2 | 3[]");
|
|
167
|
+
expect(typeValueToString(reduce!.cases[0].result)).toBe("6");
|
|
168
|
+
});
|
|
169
|
+
});
|
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
import { describe, it, expect, beforeAll } from "vitest";
|
|
2
|
+
import { parse, extractDirectives, type CaseDirective, parseTypeValueExpr } from "@nudojs/parser";
|
|
3
|
+
import { typeValueToString, createEnvironment, T } from "@nudojs/core";
|
|
4
|
+
import { evaluateFunctionFull } from "../evaluator.js";
|
|
5
|
+
|
|
6
|
+
function runNudoTest(source: string): { name: string; caseName: string; result: string }[] {
|
|
7
|
+
const ast = parse(source);
|
|
8
|
+
const directives = extractDirectives(ast);
|
|
9
|
+
const env = createEnvironment();
|
|
10
|
+
const results: { name: string; caseName: string; result: string }[] = [];
|
|
11
|
+
|
|
12
|
+
for (const fn of directives) {
|
|
13
|
+
for (const d of fn.directives) {
|
|
14
|
+
if (d.kind === "mock") {
|
|
15
|
+
if (d.arrowFn) {
|
|
16
|
+
const fnType = T.fn(d.arrowFn.params, d.arrowFn.body, env);
|
|
17
|
+
(fnType as any)._paramPatterns = d.arrowFn.paramPatterns;
|
|
18
|
+
env.bind(d.name, fnType);
|
|
19
|
+
} else if (d.expression) {
|
|
20
|
+
env.bind(d.name, parseTypeValueExpr(d.expression));
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const caseDirectives = fn.directives.filter((d): d is CaseDirective => d.kind === "case");
|
|
26
|
+
for (const dir of caseDirectives) {
|
|
27
|
+
const result = evaluateFunctionFull(fn.node, dir.args, env);
|
|
28
|
+
results.push({
|
|
29
|
+
name: fn.name,
|
|
30
|
+
caseName: dir.name,
|
|
31
|
+
result: typeValueToString(result.value),
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
return results;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
describe("Nudo vs TypeScript 对比测试", () => {
|
|
39
|
+
const source = `
|
|
40
|
+
// 1. 数组处理管道
|
|
41
|
+
// @nudo:mock validateItem = (item) => item !== null && item !== undefined
|
|
42
|
+
// @nudo:mock transformItem = (item) => ({ ...item, processed: true })
|
|
43
|
+
// @nudo:case "pipeline" ([{ id: 1 }, null, { id: 2 }, undefined, { id: 3 }])
|
|
44
|
+
function processItems(items) {
|
|
45
|
+
return items
|
|
46
|
+
.filter(validateItem)
|
|
47
|
+
.map(transformItem);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// 2. 事件处理器
|
|
51
|
+
// @nudo:mock handler = (event) => ({ type: event.type, handled: true })
|
|
52
|
+
// @nudo:case "event" ({ type: "click", target: "button" })
|
|
53
|
+
function dispatchEvent(event) {
|
|
54
|
+
const result = handler(event);
|
|
55
|
+
return { ...result, timestamp: Date.now() };
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// 3. API 客户端
|
|
59
|
+
// @nudo:mock httpClient = (url, options) => ({ status: 200, data: { success: true } })
|
|
60
|
+
// @nudo:case "api" ("/api/users", { method: "GET" })
|
|
61
|
+
function fetchUsers(url, options) {
|
|
62
|
+
const response = httpClient(url, options);
|
|
63
|
+
if (response.status === 200) {
|
|
64
|
+
return response.data;
|
|
65
|
+
}
|
|
66
|
+
return null;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// 4. 验证器组合
|
|
70
|
+
// @nudo:mock isString = (v) => typeof v === "string"
|
|
71
|
+
// @nudo:mock isNotEmpty = (v) => v.length > 0
|
|
72
|
+
// @nudo:mock isEmail = (v) => v.includes("@")
|
|
73
|
+
// @nudo:case "valid-email" ("user@example.com")
|
|
74
|
+
// @nudo:case "empty" ("")
|
|
75
|
+
// @nudo:case "number" (42)
|
|
76
|
+
function validateEmail(value) {
|
|
77
|
+
if (!isString(value)) return { valid: false, error: "not a string" };
|
|
78
|
+
if (!isNotEmpty(value)) return { valid: false, error: "empty" };
|
|
79
|
+
if (!isEmail(value)) return { valid: false, error: "invalid format" };
|
|
80
|
+
return { valid: true, value };
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// 5. 中间件链
|
|
84
|
+
// @nudo:mock authMiddleware = (req) => ({ ...req, user: { id: 1 } })
|
|
85
|
+
// @nudo:mock logMiddleware = (req) => req
|
|
86
|
+
// @nudo:case "request" ({ path: "/api", method: "GET" })
|
|
87
|
+
function applyMiddleware(req) {
|
|
88
|
+
let result = req;
|
|
89
|
+
result = authMiddleware(result);
|
|
90
|
+
result = logMiddleware(result);
|
|
91
|
+
return result;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// 6. 数据库查询构建器
|
|
95
|
+
// @nudo:mock where = (field, op, value) => ({ field, op, value })
|
|
96
|
+
// @nudo:mock orderBy = (field, dir) => ({ field, dir })
|
|
97
|
+
// @nudo:case "query" ("age", ">", 18)
|
|
98
|
+
function buildQuery(field, op, value) {
|
|
99
|
+
const condition = where(field, op, value);
|
|
100
|
+
const sort = orderBy("name", "asc");
|
|
101
|
+
return { conditions: [condition], sort };
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// 7. 缓存装饰器
|
|
105
|
+
// @nudo:mock fetchFromDB = (id) => ({ id, name: "User " + id, cached: false })
|
|
106
|
+
// @nudo:case "user" (42)
|
|
107
|
+
function getUserWithCache(id) {
|
|
108
|
+
const user = fetchFromDB(id);
|
|
109
|
+
return { ...user, cached: true };
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// 8. 策略模式
|
|
113
|
+
// @nudo:mock pricingStrategy = (base, quantity) => base * quantity * 0.9
|
|
114
|
+
// @nudo:case "order" (100, 5)
|
|
115
|
+
function calculatePrice(basePrice, quantity) {
|
|
116
|
+
return pricingStrategy(basePrice, quantity);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// 9. 异步操作
|
|
120
|
+
// @nudo:mock asyncOperation = (data) => ({ result: data * 2 })
|
|
121
|
+
// @nudo:case "async" (21)
|
|
122
|
+
function processData(data) {
|
|
123
|
+
const result = asyncOperation(data);
|
|
124
|
+
return result;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// 10. 复杂嵌套
|
|
128
|
+
// @nudo:mock outerFn = (x) => (y) => x + y
|
|
129
|
+
// @nudo:case "curried" (10)
|
|
130
|
+
function createAdder(x) {
|
|
131
|
+
const adder = outerFn(x);
|
|
132
|
+
return adder(5);
|
|
133
|
+
}
|
|
134
|
+
`;
|
|
135
|
+
|
|
136
|
+
let results: { name: string; caseName: string; result: string }[];
|
|
137
|
+
|
|
138
|
+
beforeAll(() => {
|
|
139
|
+
results = runNudoTest(source);
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
it("1. processItems - 数组处理管道", () => {
|
|
143
|
+
const r = results.find(r => r.caseName === "pipeline")!;
|
|
144
|
+
console.log("pipeline:", r.result);
|
|
145
|
+
expect(r.result).toContain("processed: true");
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
it("2. dispatchEvent - 事件处理", () => {
|
|
149
|
+
const r = results.find(r => r.caseName === "event")!;
|
|
150
|
+
console.log("event:", r.result);
|
|
151
|
+
expect(r.result).toContain("handled: true");
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
it("3. fetchUsers - API 调用", () => {
|
|
155
|
+
const r = results.find(r => r.caseName === "api")!;
|
|
156
|
+
console.log("api:", r.result);
|
|
157
|
+
expect(r.result).toContain("success: true");
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
it("4a. validateEmail - valid", () => {
|
|
161
|
+
const r = results.find(r => r.caseName === "valid-email")!;
|
|
162
|
+
console.log("valid-email:", r.result);
|
|
163
|
+
expect(r.result).toContain("valid: true");
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
it("4b. validateEmail - empty", () => {
|
|
167
|
+
const r = results.find(r => r.caseName === "empty")!;
|
|
168
|
+
console.log("empty:", r.result);
|
|
169
|
+
expect(r.result).toContain("valid: false");
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
it("4c. validateEmail - number", () => {
|
|
173
|
+
const r = results.find(r => r.caseName === "number")!;
|
|
174
|
+
console.log("number:", r.result);
|
|
175
|
+
expect(r.result).toContain("not a string");
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
it("5. applyMiddleware - 中间件", () => {
|
|
179
|
+
const r = results.find(r => r.caseName === "request")!;
|
|
180
|
+
console.log("request:", r.result);
|
|
181
|
+
expect(r.result).toContain("user:");
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
it("6. buildQuery - 查询构建", () => {
|
|
185
|
+
const r = results.find(r => r.caseName === "query")!;
|
|
186
|
+
console.log("query:", r.result);
|
|
187
|
+
expect(r.result).toContain("conditions:");
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
it("7. getUserWithCache - 缓存", () => {
|
|
191
|
+
const r = results.find(r => r.caseName === "user")!;
|
|
192
|
+
console.log("user:", r.result);
|
|
193
|
+
expect(r.result).toContain("cached: true");
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
it("8. calculatePrice - 策略", () => {
|
|
197
|
+
const r = results.find(r => r.caseName === "order")!;
|
|
198
|
+
console.log("order:", r.result);
|
|
199
|
+
expect(r.result).toBe("450");
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
it("9. processData - 数据处理", () => {
|
|
203
|
+
const r = results.find(r => r.caseName === "async")!;
|
|
204
|
+
console.log("async:", r.result);
|
|
205
|
+
expect(r.result).toContain("result: 42");
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
it("10. createAdder - 柯里化", () => {
|
|
209
|
+
const r = results.find(r => r.caseName === "curried")!;
|
|
210
|
+
console.log("curried:", r.result);
|
|
211
|
+
expect(r.result).toBe("15");
|
|
212
|
+
});
|
|
213
|
+
|
|
214
|
+
it("输出完整结果", () => {
|
|
215
|
+
console.log("\n=== Nudo @nudo:mock 测试结果 ===\n");
|
|
216
|
+
for (const r of results) {
|
|
217
|
+
console.log(`${r.name} - ${r.caseName}: ${r.result}`);
|
|
218
|
+
}
|
|
219
|
+
console.log(`\n总计: ${results.length} 个测试用例`);
|
|
220
|
+
});
|
|
221
|
+
});
|
|
@@ -0,0 +1,250 @@
|
|
|
1
|
+
import { describe, it, expect } from "vitest";
|
|
2
|
+
import { parse, extractDirectives, type CaseDirective, parseTypeValueExpr } from "@nudojs/parser";
|
|
3
|
+
import { typeValueToString, createEnvironment, T } from "@nudojs/core";
|
|
4
|
+
import { evaluateFunctionFull } from "../evaluator.js";
|
|
5
|
+
|
|
6
|
+
function inferWithMocks(source: string): { name: string; caseName: string; result: string }[] {
|
|
7
|
+
const ast = parse(source);
|
|
8
|
+
const directives = extractDirectives(ast);
|
|
9
|
+
const env = createEnvironment();
|
|
10
|
+
const results: { name: string; caseName: string; result: string }[] = [];
|
|
11
|
+
|
|
12
|
+
for (const fn of directives) {
|
|
13
|
+
// Apply mocks first
|
|
14
|
+
for (const d of fn.directives) {
|
|
15
|
+
if (d.kind === "mock") {
|
|
16
|
+
if (d.arrowFn) {
|
|
17
|
+
const fnType = T.fn(d.arrowFn.params, d.arrowFn.body, env);
|
|
18
|
+
(fnType as any)._paramPatterns = d.arrowFn.paramPatterns;
|
|
19
|
+
env.bind(d.name, fnType);
|
|
20
|
+
} else if (d.expression) {
|
|
21
|
+
env.bind(d.name, parseTypeValueExpr(d.expression));
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const caseDirectives = fn.directives.filter((d): d is CaseDirective => d.kind === "case");
|
|
27
|
+
for (const dir of caseDirectives) {
|
|
28
|
+
const result = evaluateFunctionFull(fn.node, dir.args, env);
|
|
29
|
+
results.push({
|
|
30
|
+
name: fn.name,
|
|
31
|
+
caseName: dir.name,
|
|
32
|
+
result: typeValueToString(result.value),
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
return results;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
describe("Comprehensive Mock Tests", () => {
|
|
40
|
+
describe("Array method mocks", () => {
|
|
41
|
+
it("supports filter with mock predicate", () => {
|
|
42
|
+
const results = inferWithMocks(`
|
|
43
|
+
// @nudo:mock isPositive = (x) => x > 0
|
|
44
|
+
// @nudo:case "nums" ([-1, 2, -3, 4, -5])
|
|
45
|
+
function filterPositive(arr) {
|
|
46
|
+
return arr.filter(isPositive);
|
|
47
|
+
}
|
|
48
|
+
`);
|
|
49
|
+
// filter returns a union type of elements that pass the predicate
|
|
50
|
+
expect(results[0].result).toBe("2 | 4[]");
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
it("supports map with mock transform", () => {
|
|
54
|
+
const results = inferWithMocks(`
|
|
55
|
+
// @nudo:mock double = (x) => x * 2
|
|
56
|
+
// @nudo:case "nums" ([1, 2, 3])
|
|
57
|
+
function doubleAll(arr) {
|
|
58
|
+
return arr.map(double);
|
|
59
|
+
}
|
|
60
|
+
`);
|
|
61
|
+
expect(results[0].result).toBe("[2, 4, 6]");
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
it("supports reduce with mock reducer", () => {
|
|
65
|
+
const results = inferWithMocks(`
|
|
66
|
+
// @nudo:mock add = (a, b) => a + b
|
|
67
|
+
// @nudo:case "nums" ([1, 2, 3, 4])
|
|
68
|
+
function sumAll(arr) {
|
|
69
|
+
return arr.reduce(add, 0);
|
|
70
|
+
}
|
|
71
|
+
`);
|
|
72
|
+
expect(results[0].result).toBe("10");
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
it("supports find with mock predicate", () => {
|
|
76
|
+
const results = inferWithMocks(`
|
|
77
|
+
// @nudo:mock isEven = (x) => x % 2 === 0
|
|
78
|
+
// @nudo:case "nums" ([1, 2, 3, 4, 5])
|
|
79
|
+
function findFirstEven(arr) {
|
|
80
|
+
return arr.find(isEven);
|
|
81
|
+
}
|
|
82
|
+
`);
|
|
83
|
+
// find returns a union of all possible values plus undefined
|
|
84
|
+
expect(results[0].result).toBe("1 | 2 | 3 | 4 | 5 | undefined");
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
it("supports some with mock predicate", () => {
|
|
88
|
+
const results = inferWithMocks(`
|
|
89
|
+
// @nudo:mock isNegative = (x) => x < 0
|
|
90
|
+
// @nudo:case "nums" ([1, 2, 3])
|
|
91
|
+
function hasNegative(arr) {
|
|
92
|
+
return arr.some(isNegative);
|
|
93
|
+
}
|
|
94
|
+
`);
|
|
95
|
+
expect(results[0].result).toBe("false");
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
it("supports every with mock predicate", () => {
|
|
99
|
+
const results = inferWithMocks(`
|
|
100
|
+
// @nudo:mock isPositive = (x) => x > 0
|
|
101
|
+
// @nudo:case "nums" ([1, 2, 3])
|
|
102
|
+
function allPositive(arr) {
|
|
103
|
+
return arr.every(isPositive);
|
|
104
|
+
}
|
|
105
|
+
`);
|
|
106
|
+
expect(results[0].result).toBe("true");
|
|
107
|
+
});
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
describe("String method mocks", () => {
|
|
111
|
+
it("supports split with mock separator", () => {
|
|
112
|
+
const results = inferWithMocks(`
|
|
113
|
+
// @nudo:mock separator = (x) => x === ","
|
|
114
|
+
// @nudo:case "str" ("a,b,c")
|
|
115
|
+
function splitByComma(str) {
|
|
116
|
+
return str.split(separator);
|
|
117
|
+
}
|
|
118
|
+
`);
|
|
119
|
+
// split with a function is not standard, but we test the mock mechanism
|
|
120
|
+
expect(results[0].result).toBeDefined();
|
|
121
|
+
});
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
describe("Higher-order function patterns", () => {
|
|
125
|
+
it("supports function composition", () => {
|
|
126
|
+
const results = inferWithMocks(`
|
|
127
|
+
// @nudo:mock f = (x) => x + 1
|
|
128
|
+
// @nudo:mock g = (x) => x * 2
|
|
129
|
+
// @nudo:case "num" (5)
|
|
130
|
+
function compose(x) {
|
|
131
|
+
return f(g(x));
|
|
132
|
+
}
|
|
133
|
+
`);
|
|
134
|
+
// g(5) = 10, f(10) = 11
|
|
135
|
+
expect(results[0].result).toBe("11");
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
it("supports function as return value", () => {
|
|
139
|
+
const results = inferWithMocks(`
|
|
140
|
+
// @nudo:mock multiplier = (factor) => (x) => x * factor
|
|
141
|
+
// @nudo:case "num" (5)
|
|
142
|
+
function getDouble(x) {
|
|
143
|
+
const double = multiplier(2);
|
|
144
|
+
return double(x);
|
|
145
|
+
}
|
|
146
|
+
`);
|
|
147
|
+
// This tests that mock can return functions
|
|
148
|
+
expect(results[0].result).toBeDefined();
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
it("supports callback pattern", () => {
|
|
152
|
+
const results = inferWithMocks(`
|
|
153
|
+
// @nudo:mock callback = (result) => result
|
|
154
|
+
// @nudo:case "num" (42)
|
|
155
|
+
function processWithCallback(x) {
|
|
156
|
+
const processed = x * 2;
|
|
157
|
+
return callback(processed);
|
|
158
|
+
}
|
|
159
|
+
`);
|
|
160
|
+
// callback(84) should return 84
|
|
161
|
+
expect(results[0].result).toBe("84");
|
|
162
|
+
});
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
describe("Multiple mock interactions", () => {
|
|
166
|
+
it("supports multiple mocks working together", () => {
|
|
167
|
+
const results = inferWithMocks(`
|
|
168
|
+
// @nudo:mock predicate = (x) => x > 2
|
|
169
|
+
// @nudo:mock transform = (x) => x * 10
|
|
170
|
+
// @nudo:case "nums" ([1, 2, 3, 4, 5])
|
|
171
|
+
function filterAndTransform(arr) {
|
|
172
|
+
const filtered = [];
|
|
173
|
+
for (const item of arr) {
|
|
174
|
+
if (predicate(item)) {
|
|
175
|
+
filtered.push(transform(item));
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
return filtered;
|
|
179
|
+
}
|
|
180
|
+
`);
|
|
181
|
+
// Filter items > 2: [3, 4, 5], then transform: [30, 40, 50]
|
|
182
|
+
expect(results[0].result).toBe("[30, 40, 50]");
|
|
183
|
+
});
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
describe("Edge cases", () => {
|
|
187
|
+
it("handles mock with no parameters", () => {
|
|
188
|
+
const results = inferWithMocks(`
|
|
189
|
+
// @nudo:mock getter = () => 42
|
|
190
|
+
// @nudo:case "empty" ()
|
|
191
|
+
function getValue() {
|
|
192
|
+
return getter();
|
|
193
|
+
}
|
|
194
|
+
`);
|
|
195
|
+
expect(results[0].result).toBe("42");
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
it("handles mock with rest parameters", () => {
|
|
199
|
+
const results = inferWithMocks(`
|
|
200
|
+
// @nudo:mock sum = (...args) => args.length
|
|
201
|
+
// @nudo:case "nums" ([1, 2, 3])
|
|
202
|
+
function countArgs(arr) {
|
|
203
|
+
return sum(...arr);
|
|
204
|
+
}
|
|
205
|
+
`);
|
|
206
|
+
// Rest parameters collect all arguments into a tuple
|
|
207
|
+
expect(results[0].result).toBe("3");
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
it("handles mock with complex expressions", () => {
|
|
211
|
+
const results = inferWithMocks(`
|
|
212
|
+
// @nudo:mock calc = (a, b) => (a + b) * (a - b)
|
|
213
|
+
// @nudo:case "nums" (5, 3)
|
|
214
|
+
function calculate(a, b) {
|
|
215
|
+
return calc(a, b);
|
|
216
|
+
}
|
|
217
|
+
`);
|
|
218
|
+
// calc(5, 3) = (5+3) * (5-3) = 8 * 2 = 16
|
|
219
|
+
expect(results[0].result).toBe("16");
|
|
220
|
+
});
|
|
221
|
+
});
|
|
222
|
+
|
|
223
|
+
describe("Integration with type narrowing", () => {
|
|
224
|
+
it("supports mock with typeof narrowing", () => {
|
|
225
|
+
const results = inferWithMocks(`
|
|
226
|
+
// @nudo:mock processor = (x) => typeof x === "string" ? x.toUpperCase() : x
|
|
227
|
+
// @nudo:case "str" ("hello")
|
|
228
|
+
// @nudo:case "num" (42)
|
|
229
|
+
function process(x) {
|
|
230
|
+
return processor(x);
|
|
231
|
+
}
|
|
232
|
+
`);
|
|
233
|
+
expect(results[0].result).toBe('"HELLO"');
|
|
234
|
+
expect(results[1].result).toBe("42");
|
|
235
|
+
});
|
|
236
|
+
|
|
237
|
+
it("supports mock with conditional logic", () => {
|
|
238
|
+
const results = inferWithMocks(`
|
|
239
|
+
// @nudo:mock validator = (x) => x > 0 && x < 100
|
|
240
|
+
// @nudo:case "valid" (50)
|
|
241
|
+
// @nudo:case "invalid" (150)
|
|
242
|
+
function validate(x) {
|
|
243
|
+
return validator(x);
|
|
244
|
+
}
|
|
245
|
+
`);
|
|
246
|
+
expect(results[0].result).toBe("true");
|
|
247
|
+
expect(results[1].result).toBe("false");
|
|
248
|
+
});
|
|
249
|
+
});
|
|
250
|
+
});
|