@nudojs/service 0.2.1 → 0.3.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.d.ts +517 -4
- package/dist/index.js +3043 -213
- package/package.json +16 -5
- package/CHANGELOG.md +0 -38
- package/src/__tests__/analyzer.test.ts +0 -812
- package/src/__tests__/as-replace.test.ts +0 -165
- package/src/__tests__/case-emitter.test.ts +0 -636
- package/src/__tests__/diagnostics.test.ts +0 -90
- package/src/__tests__/dts-generator.test.ts +0 -382
- package/src/__tests__/fn-sig-impl.test.ts +0 -247
- package/src/__tests__/guard-generator.test.ts +0 -29
- package/src/__tests__/integration.test.ts +0 -332
- package/src/__tests__/schema-generator.test.ts +0 -38
- package/src/analyzer.ts +0 -1841
- package/src/case-emitter.ts +0 -475
- package/src/dts-generator.ts +0 -291
- package/src/guard-generator.ts +0 -50
- package/src/index.ts +0 -47
- package/src/schema-generator.ts +0 -43
- package/tsconfig.json +0 -13
|
@@ -1,90 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect } from "vitest";
|
|
2
|
-
import { analyzeFile } from "../analyzer.ts";
|
|
3
|
-
|
|
4
|
-
describe("Diagnostic error codes and suggestions", () => {
|
|
5
|
-
it("should include code for unreachable diagnostics", () => {
|
|
6
|
-
const source = `
|
|
7
|
-
// @nudo:case "test" (1)
|
|
8
|
-
function fn(x) {
|
|
9
|
-
return x;
|
|
10
|
-
return x + 1;
|
|
11
|
-
}
|
|
12
|
-
`;
|
|
13
|
-
const result = analyzeFile("test.js", source);
|
|
14
|
-
const unreachable = result.diagnostics.find(d => d.message.includes("unreachable"));
|
|
15
|
-
expect(unreachable).toBeDefined();
|
|
16
|
-
expect(unreachable!.code).toBe("nudo-unreachable");
|
|
17
|
-
});
|
|
18
|
-
|
|
19
|
-
it("should include suggestions for unreachable diagnostics", () => {
|
|
20
|
-
const source = `
|
|
21
|
-
// @nudo:case "test" (1)
|
|
22
|
-
function fn(x) {
|
|
23
|
-
return x;
|
|
24
|
-
return x + 1;
|
|
25
|
-
}
|
|
26
|
-
`;
|
|
27
|
-
const result = analyzeFile("test.js", source);
|
|
28
|
-
const unreachable = result.diagnostics.find(d => d.message.includes("unreachable"));
|
|
29
|
-
expect(unreachable!.suggestions).toBeDefined();
|
|
30
|
-
expect(unreachable!.suggestions!.length).toBeGreaterThan(0);
|
|
31
|
-
});
|
|
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
|
-
it("should generate nudo:builtin-unknown for uncovered APIs", () => {
|
|
64
|
-
const source = `
|
|
65
|
-
// @nudo:case "test" ()
|
|
66
|
-
function fn() {
|
|
67
|
-
return WeakRef;
|
|
68
|
-
}
|
|
69
|
-
`;
|
|
70
|
-
const result = analyzeFile("test.js", source);
|
|
71
|
-
const builtin = result.diagnostics.find(d => d.code === "nudo:builtin-unknown");
|
|
72
|
-
expect(builtin).toBeDefined();
|
|
73
|
-
expect(builtin!.suggestions).toBeDefined();
|
|
74
|
-
expect(builtin!.suggestions![0]).toContain("@nudo:mock");
|
|
75
|
-
});
|
|
76
|
-
|
|
77
|
-
it("should generate nudo:mock-invalid for unparseable mock expressions", () => {
|
|
78
|
-
const source = `
|
|
79
|
-
// @nudo:mock handler = invalid!@#syntax()
|
|
80
|
-
// @nudo:case "test" (1)
|
|
81
|
-
function fn(x) {
|
|
82
|
-
return handler(x);
|
|
83
|
-
}
|
|
84
|
-
`;
|
|
85
|
-
const result = analyzeFile("test.js", source);
|
|
86
|
-
const mockDiag = result.diagnostics.find(d => d.code === "nudo:mock-invalid");
|
|
87
|
-
expect(mockDiag).toBeDefined();
|
|
88
|
-
expect(mockDiag!.suggestions).toBeDefined();
|
|
89
|
-
});
|
|
90
|
-
});
|
|
@@ -1,382 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect } from "vitest";
|
|
2
|
-
import { T } from "@nudojs/core";
|
|
3
|
-
import { typeValueToTSType, generateDts } from "../dts-generator.ts";
|
|
4
|
-
import { analyzeFile } from "../analyzer.ts";
|
|
5
|
-
|
|
6
|
-
describe("typeValueToTSType", () => {
|
|
7
|
-
it("converts literal number", () => {
|
|
8
|
-
expect(typeValueToTSType(T.literal(42))).toBe("42");
|
|
9
|
-
});
|
|
10
|
-
|
|
11
|
-
it("converts literal string", () => {
|
|
12
|
-
expect(typeValueToTSType(T.literal("hello"))).toBe('"hello"');
|
|
13
|
-
});
|
|
14
|
-
|
|
15
|
-
it("converts literal boolean", () => {
|
|
16
|
-
expect(typeValueToTSType(T.literal(true))).toBe("true");
|
|
17
|
-
});
|
|
18
|
-
|
|
19
|
-
it("converts null and undefined", () => {
|
|
20
|
-
expect(typeValueToTSType(T.null)).toBe("null");
|
|
21
|
-
expect(typeValueToTSType(T.undefined)).toBe("undefined");
|
|
22
|
-
});
|
|
23
|
-
|
|
24
|
-
it("converts primitive types", () => {
|
|
25
|
-
expect(typeValueToTSType(T.number)).toBe("number");
|
|
26
|
-
expect(typeValueToTSType(T.string)).toBe("string");
|
|
27
|
-
expect(typeValueToTSType(T.boolean)).toBe("boolean");
|
|
28
|
-
});
|
|
29
|
-
|
|
30
|
-
it("converts object type", () => {
|
|
31
|
-
const obj = T.object({ x: T.number, y: T.string });
|
|
32
|
-
expect(typeValueToTSType(obj)).toBe("{ x: number; y: string }");
|
|
33
|
-
});
|
|
34
|
-
|
|
35
|
-
it("converts array type", () => {
|
|
36
|
-
expect(typeValueToTSType(T.array(T.number))).toBe("number[]");
|
|
37
|
-
});
|
|
38
|
-
|
|
39
|
-
it("converts union array type with parens", () => {
|
|
40
|
-
expect(typeValueToTSType(T.array(T.union(T.number, T.string)))).toBe("(number | string)[]");
|
|
41
|
-
});
|
|
42
|
-
|
|
43
|
-
it("converts tuple type", () => {
|
|
44
|
-
expect(typeValueToTSType(T.tuple([T.number, T.string]))).toBe("[number, string]");
|
|
45
|
-
});
|
|
46
|
-
|
|
47
|
-
it("converts promise type", () => {
|
|
48
|
-
expect(typeValueToTSType(T.promise(T.number))).toBe("Promise<number>");
|
|
49
|
-
});
|
|
50
|
-
|
|
51
|
-
it("converts instance type", () => {
|
|
52
|
-
expect(typeValueToTSType(T.instanceOf("Error"))).toBe("Error");
|
|
53
|
-
});
|
|
54
|
-
|
|
55
|
-
it("converts union type", () => {
|
|
56
|
-
expect(typeValueToTSType(T.union(T.number, T.string))).toBe("number | string");
|
|
57
|
-
});
|
|
58
|
-
|
|
59
|
-
it("converts never and unknown", () => {
|
|
60
|
-
expect(typeValueToTSType(T.never)).toBe("never");
|
|
61
|
-
expect(typeValueToTSType(T.unknown)).toBe("unknown");
|
|
62
|
-
});
|
|
63
|
-
});
|
|
64
|
-
|
|
65
|
-
describe("generateDts", () => {
|
|
66
|
-
it("generates .d.ts from analysis result", () => {
|
|
67
|
-
const source = `
|
|
68
|
-
/**
|
|
69
|
-
* @nudo:case "concrete" (1, 2)
|
|
70
|
-
* @nudo:case "symbolic" (T.number, T.number)
|
|
71
|
-
*/
|
|
72
|
-
function add(a, b) {
|
|
73
|
-
return a + b;
|
|
74
|
-
}
|
|
75
|
-
`;
|
|
76
|
-
const result = analyzeFile("/test/gen.js", source);
|
|
77
|
-
const dts = generateDts(result);
|
|
78
|
-
// 行为已修复:不再按 case 生成字面量重载(`): 3;` 会拦截 safeSqrt(5)
|
|
79
|
-
// 这类合法调用),改为单一 widen 主签名,字面量精度保留在 JSDoc Case: 行
|
|
80
|
-
expect(dts).toContain("export declare function add(a: number, b: number): number;");
|
|
81
|
-
expect(dts.match(/export declare function add\b/g)).toHaveLength(1);
|
|
82
|
-
expect(dts).toContain("Case: concrete (1, 2) => 3");
|
|
83
|
-
// symbolic case 与主签名同形(无信息损失),不再重复罗列
|
|
84
|
-
expect(dts).not.toContain("): 3;");
|
|
85
|
-
expect(dts).not.toContain("Case: symbolic");
|
|
86
|
-
});
|
|
87
|
-
|
|
88
|
-
it("generates single overload for single case", () => {
|
|
89
|
-
const source = `
|
|
90
|
-
/**
|
|
91
|
-
* @nudo:case "test" (T.number)
|
|
92
|
-
*/
|
|
93
|
-
function identity(x) {
|
|
94
|
-
return x;
|
|
95
|
-
}
|
|
96
|
-
`;
|
|
97
|
-
const result = analyzeFile("/test/single.js", source);
|
|
98
|
-
const dts = generateDts(result);
|
|
99
|
-
expect(dts).toContain("export declare function identity");
|
|
100
|
-
expect(dts).toContain("x: number");
|
|
101
|
-
});
|
|
102
|
-
|
|
103
|
-
it("uses actual parameter names from AST", () => {
|
|
104
|
-
const source = `
|
|
105
|
-
/**
|
|
106
|
-
* @nudo:case "numbers" (T.number, T.number)
|
|
107
|
-
*/
|
|
108
|
-
function add(a, b) {
|
|
109
|
-
return a + b;
|
|
110
|
-
}
|
|
111
|
-
`;
|
|
112
|
-
const result = analyzeFile("/test/params.js", source);
|
|
113
|
-
const dts = generateDts(result);
|
|
114
|
-
expect(dts).toContain("a: number");
|
|
115
|
-
expect(dts).toContain("b: number");
|
|
116
|
-
expect(dts).not.toContain("arg0");
|
|
117
|
-
expect(dts).not.toContain("arg1");
|
|
118
|
-
});
|
|
119
|
-
|
|
120
|
-
it("generates JSDoc comments", () => {
|
|
121
|
-
const source = `
|
|
122
|
-
/**
|
|
123
|
-
* @nudo:case "test" (T.string)
|
|
124
|
-
*/
|
|
125
|
-
function greet(name) {
|
|
126
|
-
return "hello " + name;
|
|
127
|
-
}
|
|
128
|
-
`;
|
|
129
|
-
const result = analyzeFile("/test/jsdoc.js", source);
|
|
130
|
-
const dts = generateDts(result);
|
|
131
|
-
expect(dts).toContain("/**");
|
|
132
|
-
expect(dts).toContain("* @param name - string");
|
|
133
|
-
expect(dts).toContain("* @returns");
|
|
134
|
-
expect(dts).toContain("*/");
|
|
135
|
-
});
|
|
136
|
-
|
|
137
|
-
it("generates JSDoc with case names for multiple overloads", () => {
|
|
138
|
-
const source = `
|
|
139
|
-
/**
|
|
140
|
-
* @nudo:case "str" (T.string) => T.number
|
|
141
|
-
* @nudo:case "num" (T.number) => T.string
|
|
142
|
-
*/
|
|
143
|
-
function convert(x) {
|
|
144
|
-
return typeof x === "string" ? Number(x) : String(x);
|
|
145
|
-
}
|
|
146
|
-
`;
|
|
147
|
-
const result = analyzeFile("/test/multi.js", source);
|
|
148
|
-
const dts = generateDts(result);
|
|
149
|
-
expect(dts).toContain("Case: str");
|
|
150
|
-
expect(dts).toContain("Case: num");
|
|
151
|
-
});
|
|
152
|
-
|
|
153
|
-
it("handles rest parameters", () => {
|
|
154
|
-
const source = `
|
|
155
|
-
/**
|
|
156
|
-
* @nudo:case "test" (T.array(T.number))
|
|
157
|
-
*/
|
|
158
|
-
function sum(...nums) {
|
|
159
|
-
return nums;
|
|
160
|
-
}
|
|
161
|
-
`;
|
|
162
|
-
const result = analyzeFile("/test/rest.js", source);
|
|
163
|
-
const dts = generateDts(result);
|
|
164
|
-
expect(dts).toContain("...nums");
|
|
165
|
-
});
|
|
166
|
-
|
|
167
|
-
it("widens literal params so widened calls compile (safeSqrt scenario)", () => {
|
|
168
|
-
const source = `
|
|
169
|
-
/**
|
|
170
|
-
* @nudo:case "valid" (10)
|
|
171
|
-
* @nudo:case "negative" (-1)
|
|
172
|
-
*/
|
|
173
|
-
function safeSqrt(x) {
|
|
174
|
-
if (x < 0) {
|
|
175
|
-
throw new RangeError("negative input");
|
|
176
|
-
}
|
|
177
|
-
return x;
|
|
178
|
-
}
|
|
179
|
-
`;
|
|
180
|
-
const result = analyzeFile("/test/sqrt2.js", source);
|
|
181
|
-
const dts = generateDts(result);
|
|
182
|
-
// 单一 widen 主签名:参数联合 10 | -1 → number,返回 combined 同样 widen
|
|
183
|
-
expect(dts).toContain("export declare function safeSqrt(x: number): number;");
|
|
184
|
-
expect(dts.match(/export declare function safeSqrt\b/g)).toHaveLength(1);
|
|
185
|
-
// 字面量精度(含 throwing case 的 never)保留在 JSDoc
|
|
186
|
-
expect(dts).toContain("Case: valid (10) => 10");
|
|
187
|
-
expect(dts).toContain("Case: negative (-1) => never");
|
|
188
|
-
});
|
|
189
|
-
|
|
190
|
-
it("widens literal params for a single-case function too", () => {
|
|
191
|
-
const source = `
|
|
192
|
-
/**
|
|
193
|
-
* @nudo:case "only" (42)
|
|
194
|
-
*/
|
|
195
|
-
function pass(x) {
|
|
196
|
-
return x;
|
|
197
|
-
}
|
|
198
|
-
`;
|
|
199
|
-
const result = analyzeFile("/test/single-literal.js", source);
|
|
200
|
-
const dts = generateDts(result);
|
|
201
|
-
expect(dts).toContain("export declare function pass(x: number): number;");
|
|
202
|
-
expect(dts.match(/export declare function pass\b/g)).toHaveLength(1);
|
|
203
|
-
});
|
|
204
|
-
|
|
205
|
-
it("widens string and boolean literal params to base types", () => {
|
|
206
|
-
const source = `
|
|
207
|
-
/**
|
|
208
|
-
* @nudo:case "s" ("fast")
|
|
209
|
-
* @nudo:case "b" (true)
|
|
210
|
-
*/
|
|
211
|
-
function flag(x) {
|
|
212
|
-
return x;
|
|
213
|
-
}
|
|
214
|
-
`;
|
|
215
|
-
const result = analyzeFile("/test/flag.js", source);
|
|
216
|
-
const dts = generateDts(result);
|
|
217
|
-
expect(dts).toContain("export declare function flag(x: string | boolean): string | boolean;");
|
|
218
|
-
expect(dts).toContain('Case: s ("fast") => "fast"');
|
|
219
|
-
});
|
|
220
|
-
|
|
221
|
-
it("widens bigint literal params (hand-built analysis)", () => {
|
|
222
|
-
const fn = {
|
|
223
|
-
name: "big",
|
|
224
|
-
loc: { start: { line: 1, column: 0 }, end: { line: 1, column: 9 } },
|
|
225
|
-
paramNames: ["n"],
|
|
226
|
-
cases: [
|
|
227
|
-
{
|
|
228
|
-
name: "c",
|
|
229
|
-
args: [T.literal(10n)],
|
|
230
|
-
result: T.literal(10n),
|
|
231
|
-
throws: T.never,
|
|
232
|
-
},
|
|
233
|
-
],
|
|
234
|
-
};
|
|
235
|
-
const dts = generateDts({ functions: [fn] } as unknown as Parameters<typeof generateDts>[0]);
|
|
236
|
-
expect(dts).toContain("export declare function big(n: bigint): bigint;");
|
|
237
|
-
});
|
|
238
|
-
|
|
239
|
-
it("marks mixed-arity tail params optional", () => {
|
|
240
|
-
const source = `
|
|
241
|
-
/**
|
|
242
|
-
* @nudo:case "one" (T.number)
|
|
243
|
-
* @nudo:case "two" (T.number, T.string)
|
|
244
|
-
*/
|
|
245
|
-
function either(x, y) {
|
|
246
|
-
return y === undefined ? x : y;
|
|
247
|
-
}
|
|
248
|
-
`;
|
|
249
|
-
const result = analyzeFile("/test/either.js", source);
|
|
250
|
-
const dts = generateDts(result);
|
|
251
|
-
expect(dts).toContain("either(x: number, y?: string)");
|
|
252
|
-
expect(dts.match(/export declare function either\b/g)).toHaveLength(1);
|
|
253
|
-
});
|
|
254
|
-
|
|
255
|
-
it("keeps the rest-args form for case-less combined functions", () => {
|
|
256
|
-
const fn = {
|
|
257
|
-
name: "entryOnly",
|
|
258
|
-
loc: { start: { line: 1, column: 0 }, end: { line: 1, column: 9 } },
|
|
259
|
-
paramNames: ["x"],
|
|
260
|
-
cases: [],
|
|
261
|
-
combined: T.promise(T.number),
|
|
262
|
-
};
|
|
263
|
-
const dts = generateDts({ functions: [fn] } as unknown as Parameters<typeof generateDts>[0]);
|
|
264
|
-
expect(dts).toBe("export declare function entryOnly(...args: unknown[]): Promise<number>;\n");
|
|
265
|
-
});
|
|
266
|
-
|
|
267
|
-
it("widens homogeneous tuple params to arrays (reduceSum scenario)", () => {
|
|
268
|
-
const source = `
|
|
269
|
-
// @nudo:case "t" ([1, 2, 3, 4, 5])
|
|
270
|
-
function reduceSum(arr) {
|
|
271
|
-
return arr.reduce((a, b) => a + b, 0);
|
|
272
|
-
}
|
|
273
|
-
`;
|
|
274
|
-
const result = analyzeFile("/test/reduce.js", source);
|
|
275
|
-
const dts = generateDts(result);
|
|
276
|
-
// 行为已修复:定长 widened 元组 [number×5] 会拒收变长合法实参
|
|
277
|
-
// (tsc TS2345),字面量长度只是单次调用观察,不构成约束 → number[]
|
|
278
|
-
expect(dts).toContain("export declare function reduceSum(arr: number[]): number;");
|
|
279
|
-
expect(dts).toContain("Case: t ([1, 2, 3, 4, 5]) => 15");
|
|
280
|
-
});
|
|
281
|
-
|
|
282
|
-
it("widens heterogeneous tuple params positionally", () => {
|
|
283
|
-
const source = `
|
|
284
|
-
// @nudo:case "t" ([1, "two"])
|
|
285
|
-
function pair(p) {
|
|
286
|
-
return p;
|
|
287
|
-
}
|
|
288
|
-
`;
|
|
289
|
-
const result = analyzeFile("/test/pair.js", source);
|
|
290
|
-
const dts = generateDts(result);
|
|
291
|
-
// 异构元素保留 widened 定长元组:[number, string] 的位置语义真实,
|
|
292
|
-
// 且不拦截合法的按位调用;返回位(协变)保留字面量精度
|
|
293
|
-
expect(dts).toContain("export declare function pair(p: [number, string]): [1, \"two\"];");
|
|
294
|
-
});
|
|
295
|
-
|
|
296
|
-
it("widens object param property values (single case)", () => {
|
|
297
|
-
const source = `
|
|
298
|
-
/**
|
|
299
|
-
* @nudo:case "concrete" ({ name: "Alice", age: 30 })
|
|
300
|
-
*/
|
|
301
|
-
function greet(person) {
|
|
302
|
-
return \`Hello, \${person.name}! You are \${person.age} years old.\`;
|
|
303
|
-
}
|
|
304
|
-
`;
|
|
305
|
-
const result = analyzeFile("/test/greet1.js", source);
|
|
306
|
-
const dts = generateDts(result);
|
|
307
|
-
// 单 case 对象参数:属性值递归 widen(否则 greet({ name: "Bob", age: 20 })
|
|
308
|
-
// 不匹配 { name: "Alice"; age: 30 },同类拦截问题)
|
|
309
|
-
expect(dts).toContain("export declare function greet(person: { name: string; age: number }): string;");
|
|
310
|
-
expect(dts).toContain('Case: concrete ({ name: "Alice"; age: 30 })');
|
|
311
|
-
});
|
|
312
|
-
|
|
313
|
-
it("keeps return-position precision while widening params (contrast)", () => {
|
|
314
|
-
const source = `
|
|
315
|
-
// @nudo:case "t" ([1, 2])
|
|
316
|
-
function tupleEcho(arr) {
|
|
317
|
-
return arr;
|
|
318
|
-
}
|
|
319
|
-
`;
|
|
320
|
-
const result = analyzeFile("/test/echo.js", source);
|
|
321
|
-
const dts = generateDts(result);
|
|
322
|
-
// 参数位(逆变)递归 widen → number[];返回位(协变)保留字面量精度
|
|
323
|
-
expect(dts).toContain("export declare function tupleEcho(arr: number[]): [1, 2];");
|
|
324
|
-
});
|
|
325
|
-
|
|
326
|
-
it("dedupes same-shaped widened object params across cases", () => {
|
|
327
|
-
const source = `
|
|
328
|
-
/**
|
|
329
|
-
* @nudo:case "a" ({ id: 1 })
|
|
330
|
-
* @nudo:case "b" ({ id: 2 })
|
|
331
|
-
*/
|
|
332
|
-
function byId(q) {
|
|
333
|
-
return q.id;
|
|
334
|
-
}
|
|
335
|
-
`;
|
|
336
|
-
const result = analyzeFile("/test/byid.js", source);
|
|
337
|
-
const dts = generateDts(result);
|
|
338
|
-
// 两个同形对象 widen 后都成 { id: number },按渲染串去重避免
|
|
339
|
-
// `{ id: number } | { id: number }` 重复成员
|
|
340
|
-
expect(dts).toContain("export declare function byId(q: { id: number }): number;");
|
|
341
|
-
expect(dts).not.toContain("{ id: number } | { id: number }");
|
|
342
|
-
});
|
|
343
|
-
|
|
344
|
-
it("dedupes extracted underscore names from destructured params", () => {
|
|
345
|
-
const fn = {
|
|
346
|
-
name: "pair",
|
|
347
|
-
loc: { start: { line: 1, column: 0 }, end: { line: 1, column: 9 } },
|
|
348
|
-
// 两个解构参数在 AST 提取时都叫 "_";重名会让 .d.ts 非法(TS2300)
|
|
349
|
-
paramNames: ["_", "_"],
|
|
350
|
-
cases: [
|
|
351
|
-
{
|
|
352
|
-
name: "c",
|
|
353
|
-
args: [T.object({ a: T.number }), T.object({ b: T.string })],
|
|
354
|
-
result: T.number,
|
|
355
|
-
throws: T.never,
|
|
356
|
-
},
|
|
357
|
-
],
|
|
358
|
-
};
|
|
359
|
-
const dts = generateDts({ functions: [fn] } as unknown as Parameters<typeof generateDts>[0]);
|
|
360
|
-
expect(dts).toContain("export declare function pair(_: { a: number }, _2: { b: string }): number;");
|
|
361
|
-
expect(dts).toContain("@param _2 - { b: string }");
|
|
362
|
-
});
|
|
363
|
-
|
|
364
|
-
it("keeps nested literal precision inside Promise payloads", () => {
|
|
365
|
-
const fn = {
|
|
366
|
-
name: "fetchData",
|
|
367
|
-
loc: { start: { line: 1, column: 0 }, end: { line: 1, column: 9 } },
|
|
368
|
-
paramNames: [],
|
|
369
|
-
cases: [
|
|
370
|
-
{
|
|
371
|
-
name: "test",
|
|
372
|
-
args: [],
|
|
373
|
-
result: T.promise(T.literal(42)),
|
|
374
|
-
throws: T.never,
|
|
375
|
-
},
|
|
376
|
-
],
|
|
377
|
-
};
|
|
378
|
-
const dts = generateDts({ functions: [fn] } as unknown as Parameters<typeof generateDts>[0]);
|
|
379
|
-
// widen 只作用于顶层标量字面量;Promise 载荷精度保留(既有行为)
|
|
380
|
-
expect(dts).toContain("export declare function fetchData(): Promise<42>;");
|
|
381
|
-
});
|
|
382
|
-
});
|