@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,110 @@
|
|
|
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
|
+
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("Rest Parameter Debug", () => {
|
|
39
|
+
it("rest param: collect args into tuple", () => {
|
|
40
|
+
const results = inferWithMocks(`
|
|
41
|
+
// @nudo:mock sum = (...args) => args.length
|
|
42
|
+
// @nudo:case "three" (1, 2, 3)
|
|
43
|
+
function countArgs() {
|
|
44
|
+
return sum(1, 2, 3);
|
|
45
|
+
}
|
|
46
|
+
`);
|
|
47
|
+
console.log("Rest param result:", results[0].result);
|
|
48
|
+
expect(results[0].result).toBe("3");
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
it("rest param: spread array", () => {
|
|
52
|
+
const results = inferWithMocks(`
|
|
53
|
+
// @nudo:mock sum = (...args) => args.length
|
|
54
|
+
// @nudo:case "arr" ([1, 2, 3])
|
|
55
|
+
function countFromArr(arr) {
|
|
56
|
+
return sum(...arr);
|
|
57
|
+
}
|
|
58
|
+
`);
|
|
59
|
+
console.log("Spread array result:", results[0].result);
|
|
60
|
+
expect(results[0].result).toBe("3");
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
it("rest param: access elements", () => {
|
|
64
|
+
const results = inferWithMocks(`
|
|
65
|
+
// @nudo:mock getFirst = (...args) => args[0]
|
|
66
|
+
// @nudo:case "nums" (10, 20, 30)
|
|
67
|
+
function first() {
|
|
68
|
+
return getFirst(10, 20, 30);
|
|
69
|
+
}
|
|
70
|
+
`);
|
|
71
|
+
console.log("First element result:", results[0].result);
|
|
72
|
+
expect(results[0].result).toBe("10");
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
it("rest param: sum values", () => {
|
|
76
|
+
const results = inferWithMocks(`
|
|
77
|
+
// @nudo:mock add = (...args) => args[0] + args[1] + args[2]
|
|
78
|
+
// @nudo:case "nums" (1, 2, 3)
|
|
79
|
+
function addThree() {
|
|
80
|
+
return add(1, 2, 3);
|
|
81
|
+
}
|
|
82
|
+
`);
|
|
83
|
+
console.log("Sum result:", results[0].result);
|
|
84
|
+
expect(results[0].result).toBe("6");
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
it("named + rest params", () => {
|
|
88
|
+
const results = inferWithMocks(`
|
|
89
|
+
// @nudo:mock log = (first, ...rest) => rest.length
|
|
90
|
+
// @nudo:case "args" ("a", "b", "c", "d")
|
|
91
|
+
function logCount() {
|
|
92
|
+
return log("a", "b", "c", "d");
|
|
93
|
+
}
|
|
94
|
+
`);
|
|
95
|
+
console.log("Named + rest result:", results[0].result);
|
|
96
|
+
expect(results[0].result).toBe("3");
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
it("rest param with reduce", () => {
|
|
100
|
+
const results = inferWithMocks(`
|
|
101
|
+
// @nudo:mock sum = (...args) => args.reduce((a, b) => a + b, 0)
|
|
102
|
+
// @nudo:case "nums" (1, 2, 3)
|
|
103
|
+
function total() {
|
|
104
|
+
return sum(1, 2, 3);
|
|
105
|
+
}
|
|
106
|
+
`);
|
|
107
|
+
console.log("Reduce result:", results[0].result);
|
|
108
|
+
expect(results[0].result).toBe("6");
|
|
109
|
+
});
|
|
110
|
+
});
|
|
@@ -0,0 +1,176 @@
|
|
|
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
|
+
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("Rest Parameter Edge Cases", () => {
|
|
39
|
+
it("rest param: no args passed", () => {
|
|
40
|
+
const results = inferWithMocks(`
|
|
41
|
+
// @nudo:mock count = (...args) => args.length
|
|
42
|
+
// @nudo:case "empty" ()
|
|
43
|
+
function noArgs() {
|
|
44
|
+
return count();
|
|
45
|
+
}
|
|
46
|
+
`);
|
|
47
|
+
console.log("No args result:", results[0].result);
|
|
48
|
+
expect(results[0].result).toBe("0");
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
it("rest param: single arg", () => {
|
|
52
|
+
const results = inferWithMocks(`
|
|
53
|
+
// @nudo:mock first = (...args) => args[0]
|
|
54
|
+
// @nudo:case "one" (42)
|
|
55
|
+
function oneArg() {
|
|
56
|
+
return first(42);
|
|
57
|
+
}
|
|
58
|
+
`);
|
|
59
|
+
console.log("Single arg result:", results[0].result);
|
|
60
|
+
expect(results[0].result).toBe("42");
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
it("rest param: spread with mock inside higher-order fn", () => {
|
|
64
|
+
const results = inferWithMocks(`
|
|
65
|
+
// @nudo:mock fn = (x) => x * 2
|
|
66
|
+
// @nudo:case "nums" ([1, 2, 3])
|
|
67
|
+
function applyAll(arr) {
|
|
68
|
+
return arr.map(fn);
|
|
69
|
+
}
|
|
70
|
+
`);
|
|
71
|
+
console.log("Map result:", results[0].result);
|
|
72
|
+
expect(results[0].result).toBe("[2, 4, 6]");
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
it("rest param: destructure from tuple", () => {
|
|
76
|
+
const results = inferWithMocks(`
|
|
77
|
+
// @nudo:mock swap = ([a, b]) => [b, a]
|
|
78
|
+
// @nudo:case "pair" ([1, 2])
|
|
79
|
+
function swapPair(pair) {
|
|
80
|
+
return swap(pair);
|
|
81
|
+
}
|
|
82
|
+
`);
|
|
83
|
+
console.log("Destructure result:", results[0].result);
|
|
84
|
+
expect(results[0].result).toBe("[2, 1]");
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
it("rest param: nested arrow functions", () => {
|
|
88
|
+
const results = inferWithMocks(`
|
|
89
|
+
// @nudo:mock curry = (a) => (b) => a + b
|
|
90
|
+
// @nudo:case "num" (5)
|
|
91
|
+
function curried(x) {
|
|
92
|
+
const add5 = curry(x);
|
|
93
|
+
return add5(10);
|
|
94
|
+
}
|
|
95
|
+
`);
|
|
96
|
+
console.log("Curry result:", results[0].result);
|
|
97
|
+
expect(results[0].result).toBe("15");
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
it("rest param: mock returning object", () => {
|
|
101
|
+
const results = inferWithMocks(`
|
|
102
|
+
// @nudo:mock make = (x) => ({ value: x, doubled: x * 2 })
|
|
103
|
+
// @nudo:case "num" (5)
|
|
104
|
+
function makeObj(x) {
|
|
105
|
+
return make(x);
|
|
106
|
+
}
|
|
107
|
+
`);
|
|
108
|
+
console.log("Object result:", results[0].result);
|
|
109
|
+
expect(results[0].result).toBe('{ value: 5, doubled: 10 }');
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
it("rest param: conditional return (abstract interpretation)", () => {
|
|
113
|
+
const results = inferWithMocks(`
|
|
114
|
+
// @nudo:mock safe = (x) => x > 0 ? x : 0
|
|
115
|
+
// @nudo:case "pos" (5)
|
|
116
|
+
// @nudo:case "neg" (-3)
|
|
117
|
+
function safeValue(x) {
|
|
118
|
+
return safe(x);
|
|
119
|
+
}
|
|
120
|
+
`);
|
|
121
|
+
console.log("Conditional results:", results.map(r => r.result));
|
|
122
|
+
// Abstract interpretation: x > 0 narrows x to number (>= 1)
|
|
123
|
+
expect(results[0].result).toBe("number (>= 1)");
|
|
124
|
+
expect(results[1].result).toBe("0");
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
it("rest param: multiple rest params in chain", () => {
|
|
128
|
+
const results = inferWithMocks(`
|
|
129
|
+
// @nudo:mock collect = (...args) => args
|
|
130
|
+
// @nudo:mock process = (arr) => arr.length
|
|
131
|
+
// @nudo:case "nums" (1, 2, 3, 4)
|
|
132
|
+
function chain() {
|
|
133
|
+
const collected = collect(1, 2, 3, 4);
|
|
134
|
+
return process(collected);
|
|
135
|
+
}
|
|
136
|
+
`);
|
|
137
|
+
console.log("Chain result:", results[0].result);
|
|
138
|
+
expect(results[0].result).toBe("4");
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
it("destructure: object pattern", () => {
|
|
142
|
+
const results = inferWithMocks(`
|
|
143
|
+
// @nudo:mock getFullName = ({first, last}) => first + " " + last
|
|
144
|
+
// @nudo:case "name" ({first: "John", last: "Doe"})
|
|
145
|
+
function fullName(obj) {
|
|
146
|
+
return getFullName(obj);
|
|
147
|
+
}
|
|
148
|
+
`);
|
|
149
|
+
console.log("Object destructure result:", results[0].result);
|
|
150
|
+
expect(results[0].result).toBe('"John Doe"');
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
it("destructure: nested array", () => {
|
|
154
|
+
const results = inferWithMocks(`
|
|
155
|
+
// @nudo:mock sum = ([a, b]) => a + b
|
|
156
|
+
// @nudo:case "pair" ([3, 7])
|
|
157
|
+
function sumPair(pair) {
|
|
158
|
+
return sum(pair);
|
|
159
|
+
}
|
|
160
|
+
`);
|
|
161
|
+
console.log("Nested array result:", results[0].result);
|
|
162
|
+
expect(results[0].result).toBe("10");
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
it("destructure: mixed params", () => {
|
|
166
|
+
const results = inferWithMocks(`
|
|
167
|
+
// @nudo:mock process = (x, [a, b]) => x + a + b
|
|
168
|
+
// @nudo:case "mixed" (1, [2, 3])
|
|
169
|
+
function processMixed(x, pair) {
|
|
170
|
+
return process(x, pair);
|
|
171
|
+
}
|
|
172
|
+
`);
|
|
173
|
+
console.log("Mixed params result:", results[0].result);
|
|
174
|
+
expect(results[0].result).toBe("6");
|
|
175
|
+
});
|
|
176
|
+
});
|
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
import { describe, it, expect } from "vitest";
|
|
2
|
+
import { parse, extractDirectives, type CaseDirective, type MockDirective, parseTypeValueExpr } from "@nudojs/parser";
|
|
3
|
+
import { typeValueToString, createEnvironment, T, mockHelperToTypeValue } from "@nudojs/core";
|
|
4
|
+
import { evaluateFunctionFull } from "../evaluator.js";
|
|
5
|
+
|
|
6
|
+
function createSinonMock(sinonExpr: any, env: any): any {
|
|
7
|
+
const body = { type: "BlockStatement", body: [] };
|
|
8
|
+
const fn = T.fn(["...args"], body, env);
|
|
9
|
+
|
|
10
|
+
if (sinonExpr.returnValue) {
|
|
11
|
+
(fn as any)._directReturn = sinonExpr.returnValue;
|
|
12
|
+
} else if (sinonExpr.resolvedValue) {
|
|
13
|
+
(fn as any)._directReturn = T.promise(sinonExpr.resolvedValue);
|
|
14
|
+
} else if (sinonExpr.rejectedValue) {
|
|
15
|
+
(fn as any)._directReturn = T.never;
|
|
16
|
+
} else {
|
|
17
|
+
(fn as any)._directReturn = T.unknown;
|
|
18
|
+
}
|
|
19
|
+
return fn;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function inferWithSinonMocks(source: string): { name: string; caseName: string; result: string }[] {
|
|
23
|
+
const ast = parse(source);
|
|
24
|
+
const directives = extractDirectives(ast);
|
|
25
|
+
const env = createEnvironment();
|
|
26
|
+
const results: { name: string; caseName: string; result: string }[] = [];
|
|
27
|
+
|
|
28
|
+
for (const fn of directives) {
|
|
29
|
+
for (const d of fn.directives) {
|
|
30
|
+
if (d.kind === "mock") {
|
|
31
|
+
if (d.arrowFn) {
|
|
32
|
+
const fnType = T.fn(d.arrowFn.params, d.arrowFn.body, env);
|
|
33
|
+
(fnType as any)._paramPatterns = d.arrowFn.paramPatterns;
|
|
34
|
+
env.bind(d.name, fnType);
|
|
35
|
+
} else if (d.nudoMock) {
|
|
36
|
+
const typeVal = mockHelperToTypeValue(d.nudoMock, env);
|
|
37
|
+
env.bind(d.name, typeVal);
|
|
38
|
+
} else if (d.sinonExpr) {
|
|
39
|
+
const mockFn = createSinonMock(d.sinonExpr, env);
|
|
40
|
+
env.bind(d.name, mockFn);
|
|
41
|
+
} else if (d.expression) {
|
|
42
|
+
env.bind(d.name, parseTypeValueExpr(d.expression));
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const caseDirectives = fn.directives.filter((d): d is CaseDirective => d.kind === "case");
|
|
48
|
+
for (const dir of caseDirectives) {
|
|
49
|
+
const result = evaluateFunctionFull(fn.node, dir.args, env);
|
|
50
|
+
results.push({
|
|
51
|
+
name: fn.name,
|
|
52
|
+
caseName: dir.name,
|
|
53
|
+
result: typeValueToString(result.value),
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
return results;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
describe("Sinon Mock Runtime Tests", () => {
|
|
61
|
+
it("sinon.stub() - basic stub", () => {
|
|
62
|
+
const results = inferWithSinonMocks(`
|
|
63
|
+
// @nudo:mock fetch = sinon.stub()
|
|
64
|
+
// @nudo:case "call" ("/api/data")
|
|
65
|
+
function getData(url) {
|
|
66
|
+
return fetch(url);
|
|
67
|
+
}
|
|
68
|
+
`);
|
|
69
|
+
console.log("sinon.stub() result:", results[0].result);
|
|
70
|
+
// Stub returns unknown by default
|
|
71
|
+
expect(results[0].result).toBeDefined();
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
it("sinon.stub().returns() - with return value", () => {
|
|
75
|
+
const results = inferWithSinonMocks(`
|
|
76
|
+
// @nudo:mock fetch = sinon.stub().returns({ data: "test" })
|
|
77
|
+
// @nudo:case "call" ("/api/data")
|
|
78
|
+
function getData(url) {
|
|
79
|
+
return fetch(url);
|
|
80
|
+
}
|
|
81
|
+
`);
|
|
82
|
+
console.log("sinon.stub().returns() result:", results[0].result);
|
|
83
|
+
expect(results[0].result).toBeDefined();
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
it("sinon.spy() - spy function", () => {
|
|
87
|
+
const results = inferWithSinonMocks(`
|
|
88
|
+
// @nudo:mock handler = sinon.spy()
|
|
89
|
+
// @nudo:case "event" ("click")
|
|
90
|
+
function processEvent(eventName) {
|
|
91
|
+
handler(eventName);
|
|
92
|
+
return eventName;
|
|
93
|
+
}
|
|
94
|
+
`);
|
|
95
|
+
console.log("sinon.spy() result:", results[0].result);
|
|
96
|
+
expect(results[0].result).toBe('"click"');
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
it("sinon.mock() - mock function", () => {
|
|
100
|
+
const results = inferWithSinonMocks(`
|
|
101
|
+
// @nudo:mock service = sinon.mock()
|
|
102
|
+
// @nudo:case "call" ("getData")
|
|
103
|
+
function useService(method) {
|
|
104
|
+
return service[method]();
|
|
105
|
+
}
|
|
106
|
+
`);
|
|
107
|
+
console.log("sinon.mock() result:", results[0].result);
|
|
108
|
+
expect(results[0].result).toBeDefined();
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
it("sinon.stub().resolves() - promise resolution", () => {
|
|
112
|
+
const results = inferWithSinonMocks(`
|
|
113
|
+
// @nudo:mock fetch = sinon.stub().resolves({ data: "test" })
|
|
114
|
+
// @nudo:case "call" ("/api/data")
|
|
115
|
+
async function getData(url) {
|
|
116
|
+
return await fetch(url);
|
|
117
|
+
}
|
|
118
|
+
`);
|
|
119
|
+
console.log("sinon.stub().resolves() result:", results[0].result);
|
|
120
|
+
expect(results[0].result).toBeDefined();
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
it("sinon.stub().callsFake() - executes the fake with actual args", () => {
|
|
124
|
+
const results = inferWithSinonMocks(`
|
|
125
|
+
// @nudo:mock transform = sinon.stub().callsFake((x) => ({ v: x }))
|
|
126
|
+
// @nudo:case "direct" (21)
|
|
127
|
+
function applyTransform(x) {
|
|
128
|
+
return transform(x);
|
|
129
|
+
}
|
|
130
|
+
`);
|
|
131
|
+
expect(results[0].result).toBe("{ v: 21 }");
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
it("stub().callsFake() (bare form) - same execution semantics as sinon prefix", () => {
|
|
135
|
+
const results = inferWithSinonMocks(`
|
|
136
|
+
// @nudo:mock transform = stub().callsFake((x) => ({ v: x }))
|
|
137
|
+
// @nudo:case "direct" (21)
|
|
138
|
+
function applyTransform(x) {
|
|
139
|
+
return transform(x);
|
|
140
|
+
}
|
|
141
|
+
`);
|
|
142
|
+
expect(results[0].result).toBe("{ v: 21 }");
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
it("sinon.stub().callsFake() value propagates precisely through arr.map HOF", () => {
|
|
146
|
+
const results = inferWithSinonMocks(`
|
|
147
|
+
// @nudo:mock transform = sinon.stub().callsFake((x) => ({ v: x }))
|
|
148
|
+
// @nudo:case "hof" ([1, 2, 3])
|
|
149
|
+
function mapAll(arr) {
|
|
150
|
+
return arr.map(transform);
|
|
151
|
+
}
|
|
152
|
+
`);
|
|
153
|
+
expect(results[0].result).toBe("[{ v: 1 }, { v: 2 }, { v: 3 }]");
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
it("sinon.stub().withArgs().returns() - matched args use the chain return, unmatched fall back", () => {
|
|
157
|
+
const results = inferWithSinonMocks(`
|
|
158
|
+
// @nudo:mock handler = sinon.stub().withArgs(21).returns("hit")
|
|
159
|
+
// @nudo:case "match" (21)
|
|
160
|
+
// @nudo:case "miss" (22)
|
|
161
|
+
function call(x) {
|
|
162
|
+
return handler(x);
|
|
163
|
+
}
|
|
164
|
+
`);
|
|
165
|
+
expect(results[0].result).toBe('"hit"');
|
|
166
|
+
expect(results[1].result).toBe("unknown");
|
|
167
|
+
});
|
|
168
|
+
});
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { describe, it, expect } from "vitest";
|
|
2
|
+
import { T, typeValueToString, createEnvironment } from "@nudojs/core";
|
|
3
|
+
import { parse } from "@nudojs/parser";
|
|
4
|
+
import { narrow } from "../narrowing.ts";
|
|
5
|
+
import type { ExpressionStatement } from "@babel/types";
|
|
6
|
+
|
|
7
|
+
function getTestExpr(source: string) {
|
|
8
|
+
const ast = parse(source);
|
|
9
|
+
const stmt = ast.program.body[0] as ExpressionStatement;
|
|
10
|
+
return stmt.expression;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
describe("narrow: discriminated unions", () => {
|
|
14
|
+
it("narrows shape.kind === 'circle' to circle member", () => {
|
|
15
|
+
const env = createEnvironment();
|
|
16
|
+
const circle = T.object({ kind: T.literal("circle"), radius: T.number });
|
|
17
|
+
const rect = T.object({ kind: T.literal("rect"), w: T.number, h: T.number });
|
|
18
|
+
env.bind("shape", T.union(circle, rect));
|
|
19
|
+
const expr = getTestExpr('shape.kind === "circle"');
|
|
20
|
+
const [trueEnv, falseEnv] = narrow(expr, env);
|
|
21
|
+
expect(typeValueToString(trueEnv.lookup("shape"))).toBe('{ kind: "circle", radius: number }');
|
|
22
|
+
expect(typeValueToString(falseEnv.lookup("shape"))).toBe('{ kind: "rect", w: number, h: number }');
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
it("narrows shape.kind !== 'circle' (inverted)", () => {
|
|
26
|
+
const env = createEnvironment();
|
|
27
|
+
const circle = T.object({ kind: T.literal("circle"), radius: T.number });
|
|
28
|
+
const rect = T.object({ kind: T.literal("rect"), w: T.number, h: T.number });
|
|
29
|
+
env.bind("shape", T.union(circle, rect));
|
|
30
|
+
const expr = getTestExpr('shape.kind !== "circle"');
|
|
31
|
+
const [trueEnv, falseEnv] = narrow(expr, env);
|
|
32
|
+
expect(typeValueToString(trueEnv.lookup("shape"))).toBe('{ kind: "rect", w: number, h: number }');
|
|
33
|
+
expect(typeValueToString(falseEnv.lookup("shape"))).toBe('{ kind: "circle", radius: number }');
|
|
34
|
+
});
|
|
35
|
+
});
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { describe, it, expect } from "vitest";
|
|
2
|
+
import { T, typeValueToString, createEnvironment } from "@nudojs/core";
|
|
3
|
+
import { parse } from "@nudojs/parser";
|
|
4
|
+
import { narrow } from "../narrowing.ts";
|
|
5
|
+
import { evaluateProgram } from "../evaluator.ts";
|
|
6
|
+
import type { ExpressionStatement } from "@babel/types";
|
|
7
|
+
|
|
8
|
+
function getTestExpr(source: string) {
|
|
9
|
+
const ast = parse(source);
|
|
10
|
+
const stmt = ast.program.body[0] as ExpressionStatement;
|
|
11
|
+
return stmt.expression;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
describe("narrow: in operator", () => {
|
|
15
|
+
it("narrows 'key' in obj to objects with that property", () => {
|
|
16
|
+
const env = createEnvironment();
|
|
17
|
+
const objWithFoo = T.object({ foo: T.string });
|
|
18
|
+
const objWithBar = T.object({ bar: T.number });
|
|
19
|
+
env.bind("obj", T.union(objWithFoo, objWithBar));
|
|
20
|
+
const expr = getTestExpr('"foo" in obj');
|
|
21
|
+
const [trueEnv, falseEnv] = narrow(expr, env);
|
|
22
|
+
expect(typeValueToString(trueEnv.lookup("obj"))).toBe("{ foo: string }");
|
|
23
|
+
expect(typeValueToString(falseEnv.lookup("obj"))).toBe("{ bar: number }");
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
it("returns unchanged env when right side is not a union", () => {
|
|
27
|
+
const env = createEnvironment();
|
|
28
|
+
env.bind("obj", T.object({ foo: T.string }));
|
|
29
|
+
const expr = getTestExpr('"foo" in obj');
|
|
30
|
+
const [trueEnv, falseEnv] = narrow(expr, env);
|
|
31
|
+
expect(typeValueToString(trueEnv.lookup("obj"))).toBe("{ foo: string }");
|
|
32
|
+
expect(typeValueToString(falseEnv.lookup("obj"))).toBe("{ foo: string }");
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
it("evaluates Symbol.iterator in iterable receiver to true", () => {
|
|
36
|
+
const env = createEnvironment();
|
|
37
|
+
evaluateProgram(parse("const r = (Symbol.iterator in [1, 2]);").program, env);
|
|
38
|
+
expect(typeValueToString(env.lookup("r"))).toBe("true");
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
it("evaluates Symbol.iterator in plain object to false", () => {
|
|
42
|
+
const env = createEnvironment();
|
|
43
|
+
evaluateProgram(parse("const o = { a: 1 }; const r = (Symbol.iterator in o);").program, env);
|
|
44
|
+
expect(typeValueToString(env.lookup("r"))).toBe("false");
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
it("distributes Symbol.iterator in over unions", () => {
|
|
48
|
+
const env = createEnvironment();
|
|
49
|
+
evaluateProgram(
|
|
50
|
+
parse("const flag = JSON.parse('x'); const x = flag ? [1] : 5; const r = (Symbol.iterator in x);").program,
|
|
51
|
+
env,
|
|
52
|
+
);
|
|
53
|
+
expect(typeValueToString(env.lookup("x"))).toBe("[1] | 5");
|
|
54
|
+
expect(typeValueToString(env.lookup("r"))).toBe("true | false");
|
|
55
|
+
});
|
|
56
|
+
});
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { describe, it, expect } from "vitest";
|
|
2
|
+
import { T, typeValueEquals, typeValueToString, createEnvironment } from "@nudojs/core";
|
|
3
|
+
import { parse } from "@nudojs/parser";
|
|
4
|
+
import { evaluateFunction } from "../evaluator.ts";
|
|
5
|
+
|
|
6
|
+
function getFunctionNode(source: string, name: string) {
|
|
7
|
+
const ast = parse(source);
|
|
8
|
+
const stmt = ast.program.body[0];
|
|
9
|
+
if (stmt.type === "FunctionDeclaration" && stmt.id?.name === name) return stmt;
|
|
10
|
+
throw new Error(`Function ${name} not found`);
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
describe("switch narrowing", () => {
|
|
14
|
+
it("narrows discriminated union in switch cases", () => {
|
|
15
|
+
const circle = T.object({ kind: T.literal("circle"), radius: T.number });
|
|
16
|
+
const rect = T.object({ kind: T.literal("rect"), w: T.number, h: T.number });
|
|
17
|
+
const shapeUnion = T.union(circle, rect);
|
|
18
|
+
|
|
19
|
+
const source = `
|
|
20
|
+
function getArea(shape) {
|
|
21
|
+
switch (shape.kind) {
|
|
22
|
+
case "circle": return shape.radius;
|
|
23
|
+
case "rect": return shape.w;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
`;
|
|
27
|
+
|
|
28
|
+
const fnNode = getFunctionNode(source, "getArea");
|
|
29
|
+
const env = createEnvironment();
|
|
30
|
+
const result = evaluateFunction(fnNode, [shapeUnion], env);
|
|
31
|
+
|
|
32
|
+
// shape.radius (circle) and shape.w (rect) are both number
|
|
33
|
+
expect(typeValueEquals(result, T.number)).toBe(true);
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
it("narrows typeof in switch", () => {
|
|
37
|
+
const source = `
|
|
38
|
+
function describe(x) {
|
|
39
|
+
switch (typeof x) {
|
|
40
|
+
case "string": return x.toUpperCase();
|
|
41
|
+
case "number": return x + 1;
|
|
42
|
+
default: return x;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
`;
|
|
46
|
+
|
|
47
|
+
const fnNode = getFunctionNode(source, "describe");
|
|
48
|
+
const env = createEnvironment();
|
|
49
|
+
const result = evaluateFunction(fnNode, [T.union(T.string, T.number)], env);
|
|
50
|
+
|
|
51
|
+
// string.toUpperCase() => string, number + 1 => number, default => string | number
|
|
52
|
+
// All branches return string | number
|
|
53
|
+
const expected = T.union(T.string, T.number);
|
|
54
|
+
expect(typeValueEquals(result, expected)).toBe(true);
|
|
55
|
+
});
|
|
56
|
+
});
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { describe, it, expect } from "vitest";
|
|
2
|
+
import { T, typeValueEquals, typeValueToString, createEnvironment } from "@nudojs/core";
|
|
3
|
+
import { parse } from "@nudojs/parser";
|
|
4
|
+
import { narrow } from "../narrowing.ts";
|
|
5
|
+
import type { ExpressionStatement } from "@babel/types";
|
|
6
|
+
|
|
7
|
+
function getTestExpr(source: string) {
|
|
8
|
+
const ast = parse(source);
|
|
9
|
+
const stmt = ast.program.body[0] as ExpressionStatement;
|
|
10
|
+
return stmt.expression;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
describe("narrow: truthiness", () => {
|
|
14
|
+
it("narrows if(x) to exclude null and undefined", () => {
|
|
15
|
+
const env = createEnvironment();
|
|
16
|
+
env.bind("x", T.union(T.string, T.null, T.undefined));
|
|
17
|
+
const expr = getTestExpr("x");
|
|
18
|
+
const [trueEnv, falseEnv] = narrow(expr, env);
|
|
19
|
+
expect(typeValueEquals(trueEnv.lookup("x"), T.string)).toBe(true);
|
|
20
|
+
const falseType = falseEnv.lookup("x");
|
|
21
|
+
expect(typeValueToString(falseType)).toBe("null | undefined");
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
it("narrows if(x) to exclude 0, empty string, false", () => {
|
|
25
|
+
const env = createEnvironment();
|
|
26
|
+
env.bind("x", T.union(T.number, T.string, T.boolean, T.null));
|
|
27
|
+
const expr = getTestExpr("x");
|
|
28
|
+
const [trueEnv, falseEnv] = narrow(expr, env);
|
|
29
|
+
const trueType = trueEnv.lookup("x");
|
|
30
|
+
expect(typeValueToString(trueType)).toBe("number | string | boolean");
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
it("does not narrow non-union types", () => {
|
|
34
|
+
const env = createEnvironment();
|
|
35
|
+
env.bind("x", T.string);
|
|
36
|
+
const expr = getTestExpr("x");
|
|
37
|
+
const [trueEnv, falseEnv] = narrow(expr, env);
|
|
38
|
+
expect(typeValueEquals(trueEnv.lookup("x"), T.string)).toBe(true);
|
|
39
|
+
expect(typeValueEquals(falseEnv.lookup("x"), T.string)).toBe(true);
|
|
40
|
+
});
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
describe("narrow: Array.isArray", () => {
|
|
44
|
+
it("narrows Array.isArray(x) to array type", () => {
|
|
45
|
+
const env = createEnvironment();
|
|
46
|
+
env.bind("x", T.union(T.array(T.string), T.object({ foo: T.number })));
|
|
47
|
+
const expr = getTestExpr("Array.isArray(x)");
|
|
48
|
+
const [trueEnv, falseEnv] = narrow(expr, env);
|
|
49
|
+
expect(trueEnv.lookup("x").kind).toBe("array");
|
|
50
|
+
expect(falseEnv.lookup("x").kind).toBe("object");
|
|
51
|
+
});
|
|
52
|
+
});
|