@nudojs/service 0.2.0 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (72) hide show
  1. package/CHANGELOG.md +37 -0
  2. package/README.md +27 -0
  3. package/dist/index.d.ts +638 -5
  4. package/dist/index.js +4439 -208
  5. package/package.json +17 -4
  6. package/src/__tests__/abs-bindings.test.ts +53 -0
  7. package/src/__tests__/abs-call-records.test.ts +33 -0
  8. package/src/__tests__/abs-calls.test.ts +59 -0
  9. package/src/__tests__/abs-module-cache.test.ts +121 -0
  10. package/src/__tests__/abs-modules-graph.test.ts +100 -0
  11. package/src/__tests__/abs-typevalue-diff.test.ts +111 -0
  12. package/src/__tests__/analyzer.test.ts +745 -2
  13. package/src/__tests__/as-replace.test.ts +165 -0
  14. package/src/__tests__/bpath-arg-provenance.test.ts +104 -0
  15. package/src/__tests__/bpath-as.test.ts +54 -0
  16. package/src/__tests__/bpath-cache-key.test.ts +33 -0
  17. package/src/__tests__/bpath-calls.test.ts +55 -0
  18. package/src/__tests__/bpath-diagnostics.test.ts +78 -0
  19. package/src/__tests__/bpath-env-modules.test.ts +30 -0
  20. package/src/__tests__/bpath-env.test.ts +80 -0
  21. package/src/__tests__/bpath-hosted-skip.test.ts +71 -0
  22. package/src/__tests__/bpath-import-default-ns.test.ts +106 -0
  23. package/src/__tests__/bpath-method-dedupe.test.ts +65 -0
  24. package/src/__tests__/bpath-method-diag.test.ts +45 -0
  25. package/src/__tests__/bpath-module-diags.test.ts +101 -0
  26. package/src/__tests__/bpath-path-env.test.ts +46 -0
  27. package/src/__tests__/bpath-reexport.test.ts +131 -0
  28. package/src/__tests__/bpath-replace.test.ts +52 -0
  29. package/src/__tests__/bpath-require.test.ts +66 -0
  30. package/src/__tests__/bpath-run.test.ts +109 -0
  31. package/src/__tests__/case-emitter.test.ts +636 -0
  32. package/src/__tests__/cjs-require.test.ts +54 -0
  33. package/src/__tests__/diagnostics.test.ts +60 -0
  34. package/src/__tests__/dts-generator.test.ts +290 -6
  35. package/src/__tests__/fn-cache-evict.test.ts +56 -0
  36. package/src/__tests__/fn-sig-impl.test.ts +247 -0
  37. package/src/__tests__/guard-generator.test.ts +29 -0
  38. package/src/__tests__/harvest-auto.test.ts +43 -0
  39. package/src/__tests__/harvest-node.test.ts +24 -0
  40. package/src/__tests__/harvest-package.test.ts +38 -0
  41. package/src/__tests__/harvest-to-abs.test.ts +46 -0
  42. package/src/__tests__/hover-intension.test.ts +39 -0
  43. package/src/__tests__/infer-json.test.ts +35 -0
  44. package/src/__tests__/integration.test.ts +421 -0
  45. package/src/__tests__/m3-intension.test.ts +28 -0
  46. package/src/__tests__/mock-seed-fingerprint.test.ts +64 -0
  47. package/src/__tests__/schema-generator.test.ts +38 -0
  48. package/src/__tests__/session-cache-evict.test.ts +90 -0
  49. package/src/__tests__/static-imports.test.ts +44 -0
  50. package/src/abs-modules-graph.ts +387 -0
  51. package/src/analysis-file-cache.ts +51 -0
  52. package/src/analyzer.ts +2569 -94
  53. package/src/bpath-diagnostics.ts +283 -0
  54. package/src/bpath-run.ts +409 -0
  55. package/src/case-emitter.ts +477 -0
  56. package/src/dts-generator.ts +220 -24
  57. package/src/env-to-abs.ts +57 -0
  58. package/src/fn-analysis-cache.ts +119 -0
  59. package/src/guard-generator.ts +57 -0
  60. package/src/harvest-auto.ts +86 -0
  61. package/src/harvest-node.ts +48 -0
  62. package/src/harvest-package.ts +204 -0
  63. package/src/harvest-to-abs.ts +129 -0
  64. package/src/index.ts +128 -0
  65. package/src/infer-json.ts +137 -0
  66. package/src/load-module.ts +34 -0
  67. package/src/mock-abs.ts +275 -0
  68. package/src/schema-generator.ts +43 -0
  69. package/src/semantic-tokens.ts +216 -0
  70. package/src/session-cache.ts +50 -0
  71. package/src/static-imports.ts +241 -0
  72. package/src/target-path.ts +17 -0
@@ -0,0 +1,60 @@
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 generate nudo:builtin-unknown for uncovered APIs", () => {
34
+ const source = `
35
+ // @nudo:case "test" ()
36
+ function fn() {
37
+ return WeakRef;
38
+ }
39
+ `;
40
+ const result = analyzeFile("test.js", source);
41
+ const builtin = result.diagnostics.find(d => d.code === "nudo:builtin-unknown");
42
+ expect(builtin).toBeDefined();
43
+ expect(builtin!.suggestions).toBeDefined();
44
+ expect(builtin!.suggestions![0]).toContain("@nudo:mock");
45
+ });
46
+
47
+ it("should generate nudo:mock-invalid for unparseable mock expressions", () => {
48
+ const source = `
49
+ // @nudo:mock handler = invalid!@#syntax()
50
+ // @nudo:case "test" (1)
51
+ function fn(x) {
52
+ return handler(x);
53
+ }
54
+ `;
55
+ const result = analyzeFile("test.js", source);
56
+ const mockDiag = result.diagnostics.find(d => d.code === "nudo:mock-invalid");
57
+ expect(mockDiag).toBeDefined();
58
+ expect(mockDiag!.suggestions).toBeDefined();
59
+ });
60
+ });
@@ -75,9 +75,14 @@ function add(a, b) {
75
75
  `;
76
76
  const result = analyzeFile("/test/gen.js", source);
77
77
  const dts = generateDts(result);
78
- expect(dts).toContain("export declare function add");
79
- expect(dts).toContain("): 3;");
80
- expect(dts).toContain("): number;");
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");
81
86
  });
82
87
 
83
88
  it("generates single overload for single case", () => {
@@ -91,8 +96,287 @@ function identity(x) {
91
96
  `;
92
97
  const result = analyzeFile("/test/single.js", source);
93
98
  const dts = generateDts(result);
94
- const lines = dts.trim().split("\n");
95
- expect(lines).toHaveLength(1);
96
- expect(lines[0]).toContain("export declare function identity");
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>;");
97
381
  });
98
382
  });
@@ -0,0 +1,56 @@
1
+ import { describe, it, expect, beforeEach } from "vitest";
2
+ import {
3
+ evictFnAnalysisCacheForFiles,
4
+ clearFnAnalysisCache,
5
+ fnAnalysisCacheSet,
6
+ fnAnalysisCacheGet,
7
+ type CachedFnAnalysis,
8
+ } from "../fn-analysis-cache.ts";
9
+
10
+ function entry(name: string): CachedFnAnalysis {
11
+ return {
12
+ analysis: {
13
+ name,
14
+ loc: {
15
+ start: { line: 1, column: 0 },
16
+ end: { line: 1, column: 10 },
17
+ },
18
+ paramNames: [],
19
+ cases: [],
20
+ },
21
+ diagnostics: [],
22
+ caseHints: [],
23
+ callRecords: [],
24
+ };
25
+ }
26
+
27
+ const SEP = "\0";
28
+
29
+ describe("evictFnAnalysisCacheForFiles", () => {
30
+ beforeEach(() => {
31
+ clearFnAnalysisCache();
32
+ });
33
+
34
+ it("drops every fn entry for the given entry file", () => {
35
+ const keyA1 = `/t/a.js${SEP}own1${SEP}deps1${SEP}0`;
36
+ const keyA2 = `/t/a.js${SEP}own2${SEP}deps2${SEP}0`;
37
+ const keyB = `/t/b.js${SEP}own1${SEP}deps1${SEP}0`;
38
+ fnAnalysisCacheSet(keyA1, entry("a"));
39
+ fnAnalysisCacheSet(keyA2, entry("b"));
40
+ fnAnalysisCacheSet(keyB, entry("c"));
41
+ const n = evictFnAnalysisCacheForFiles(["/t/a.js"]);
42
+ expect(n).toBe(2);
43
+ expect(fnAnalysisCacheGet(keyA1)).toBeUndefined();
44
+ expect(fnAnalysisCacheGet(keyB)).toBeDefined();
45
+ });
46
+
47
+ it("does not drop a different file with a shared path prefix", () => {
48
+ const keyA = `/t/a.js${SEP}x${SEP}y${SEP}0`;
49
+ const keyBak = `/t/a.js.bak${SEP}x${SEP}y${SEP}0`;
50
+ fnAnalysisCacheSet(keyA, entry("a"));
51
+ fnAnalysisCacheSet(keyBak, entry("bak"));
52
+ evictFnAnalysisCacheForFiles(["/t/a.js"]);
53
+ expect(fnAnalysisCacheGet(keyA)).toBeUndefined();
54
+ expect(fnAnalysisCacheGet(keyBak)).toBeDefined();
55
+ });
56
+ });
@@ -0,0 +1,247 @@
1
+ import { describe, it, expect } from "vitest";
2
+ import { typeValueToString } from "@nudojs/core";
3
+ import { analyzeFile } from "../analyzer.ts";
4
+
5
+ describe("fnSig impl: concrete args produce precise values", () => {
6
+ it("Math.floor with literal returns exact result", () => {
7
+ const source = `
8
+ /// @nudo:env es
9
+
10
+ /**
11
+ * @nudo:case "concrete" (3.7)
12
+ * @nudo:case "symbolic" (T.number)
13
+ */
14
+ function floorIt(x) {
15
+ return Math.floor(x);
16
+ }
17
+ `;
18
+ const result = analyzeFile("/test/math.js", source);
19
+ const fn = result.functions[0];
20
+ expect(fn.cases[0].name).toBe("concrete");
21
+ expect(typeValueToString(fn.cases[0].result)).toBe("3");
22
+ expect(fn.cases[1].name).toBe("symbolic");
23
+ expect(typeValueToString(fn.cases[1].result)).toBe("number");
24
+ });
25
+
26
+ it("Math.max with literals returns exact result", () => {
27
+ const source = `
28
+ /// @nudo:env es
29
+
30
+ /**
31
+ * @nudo:case "test" (3, 7)
32
+ */
33
+ function maxOf(a, b) {
34
+ return Math.max(a, b);
35
+ }
36
+ `;
37
+ const result = analyzeFile("/test/math.js", source);
38
+ expect(typeValueToString(result.functions[0].cases[0].result)).toBe("7");
39
+ });
40
+
41
+ it("JSON.stringify with literal returns exact string", () => {
42
+ const source = `
43
+ /// @nudo:env es
44
+
45
+ /**
46
+ * @nudo:case "test" (42)
47
+ */
48
+ function toJson(x) {
49
+ return JSON.stringify(x);
50
+ }
51
+ `;
52
+ const result = analyzeFile("/test/json.js", source);
53
+ expect(typeValueToString(result.functions[0].cases[0].result)).toBe('"42"');
54
+ });
55
+
56
+ it("encodeURIComponent with literal returns exact string", () => {
57
+ const source = `
58
+ /// @nudo:env es
59
+
60
+ /**
61
+ * @nudo:case "test" ("hello world")
62
+ */
63
+ function encode(s) {
64
+ return encodeURIComponent(s);
65
+ }
66
+ `;
67
+ const result = analyzeFile("/test/encode.js", source);
68
+ expect(typeValueToString(result.functions[0].cases[0].result)).toBe('"hello%20world"');
69
+ });
70
+
71
+ it("Number.isInteger with literal returns exact boolean", () => {
72
+ const source = `
73
+ /// @nudo:env es
74
+
75
+ /**
76
+ * @nudo:case "int" (5)
77
+ * @nudo:case "float" (5.5)
78
+ */
79
+ function checkInt(x) {
80
+ return Number.isInteger(x);
81
+ }
82
+ `;
83
+ const result = analyzeFile("/test/num.js", source);
84
+ expect(typeValueToString(result.functions[0].cases[0].result)).toBe("true");
85
+ expect(typeValueToString(result.functions[0].cases[1].result)).toBe("false");
86
+ });
87
+
88
+ it("parseInt with literal returns exact number", () => {
89
+ const source = `
90
+ /// @nudo:env es
91
+
92
+ /**
93
+ * @nudo:case "test" ("42")
94
+ */
95
+ function parseIt(s) {
96
+ return parseInt(s);
97
+ }
98
+ `;
99
+ const result = analyzeFile("/test/parse.js", source);
100
+ expect(typeValueToString(result.functions[0].cases[0].result)).toBe("42");
101
+ });
102
+
103
+ it("Promise.resolve preserves argument type", () => {
104
+ const source = `
105
+ /// @nudo:env es
106
+
107
+ /**
108
+ * @nudo:case "test" (42)
109
+ */
110
+ async function wrap(x) {
111
+ return await Promise.resolve(x);
112
+ }
113
+ `;
114
+ const result = analyzeFile("/test/promise.js", source);
115
+ expect(typeValueToString(result.functions[0].cases[0].result)).toBe("Promise<42>");
116
+ });
117
+ });
118
+
119
+ describe("fnSig impl: URL constructor with concrete args", () => {
120
+ it("URL with literal string returns precise properties", () => {
121
+ const source = `
122
+ /// @nudo:env web
123
+
124
+ /**
125
+ * @nudo:case "test" ("https://example.com/path?q=hello#hash")
126
+ */
127
+ function parseUrl(raw) {
128
+ const url = new URL(raw);
129
+ return {
130
+ host: url.hostname,
131
+ path: url.pathname,
132
+ query: url.search,
133
+ hash: url.hash,
134
+ };
135
+ }
136
+ `;
137
+ const result = analyzeFile("/test/url.js", source);
138
+ const res = typeValueToString(result.functions[0].cases[0].result);
139
+ expect(res).toContain('"example.com"');
140
+ expect(res).toContain('"/path"');
141
+ expect(res).toContain('"?q=hello"');
142
+ expect(res).toContain('"#hash"');
143
+ });
144
+
145
+ it("URL with symbolic string falls back to generic type", () => {
146
+ const source = `
147
+ /// @nudo:env web
148
+
149
+ /**
150
+ * @nudo:case "test" (T.string)
151
+ */
152
+ function parseUrl(raw) {
153
+ const url = new URL(raw);
154
+ return url.hostname;
155
+ }
156
+ `;
157
+ const result = analyzeFile("/test/url.js", source);
158
+ expect(typeValueToString(result.functions[0].cases[0].result)).toBe("string");
159
+ });
160
+ });
161
+
162
+ describe("fnSig impl: Node.js path with concrete args", () => {
163
+ it("path.join with literals returns exact path", () => {
164
+ const source = `
165
+ /// @nudo:env node
166
+
167
+ import { join } from "node:path";
168
+
169
+ /**
170
+ * @nudo:case "test" ("src", "utils", "index.js")
171
+ */
172
+ function buildPath(a, b, c) {
173
+ return join(a, b, c);
174
+ }
175
+ `;
176
+ const result = analyzeFile("/test/path.js", source);
177
+ expect(typeValueToString(result.functions[0].cases[0].result)).toBe('"src/utils/index.js"');
178
+ });
179
+
180
+ it("path.extname with literal returns exact extension", () => {
181
+ const source = `
182
+ /// @nudo:env node
183
+
184
+ import { extname } from "node:path";
185
+
186
+ /**
187
+ * @nudo:case "test" ("file.ts")
188
+ */
189
+ function getExt(f) {
190
+ return extname(f);
191
+ }
192
+ `;
193
+ const result = analyzeFile("/test/path.js", source);
194
+ expect(typeValueToString(result.functions[0].cases[0].result)).toBe('".ts"');
195
+ });
196
+
197
+ it("path.basename with literal returns exact name", () => {
198
+ const source = `
199
+ /// @nudo:env node
200
+
201
+ import { basename } from "node:path";
202
+
203
+ /**
204
+ * @nudo:case "test" ("/home/user/file.txt")
205
+ */
206
+ function getName(p) {
207
+ return basename(p);
208
+ }
209
+ `;
210
+ const result = analyzeFile("/test/path.js", source);
211
+ expect(typeValueToString(result.functions[0].cases[0].result)).toBe('"file.txt"');
212
+ });
213
+
214
+ it("path.dirname with literal returns exact dir", () => {
215
+ const source = `
216
+ /// @nudo:env node
217
+
218
+ import { dirname } from "node:path";
219
+
220
+ /**
221
+ * @nudo:case "test" ("/home/user/file.txt")
222
+ */
223
+ function getDir(p) {
224
+ return dirname(p);
225
+ }
226
+ `;
227
+ const result = analyzeFile("/test/path.js", source);
228
+ expect(typeValueToString(result.functions[0].cases[0].result)).toBe('"/home/user"');
229
+ });
230
+
231
+ it("path.join with symbolic args falls back to string", () => {
232
+ const source = `
233
+ /// @nudo:env node
234
+
235
+ import { join } from "node:path";
236
+
237
+ /**
238
+ * @nudo:case "test" (T.string, T.string)
239
+ */
240
+ function buildPath(a, b) {
241
+ return join(a, b);
242
+ }
243
+ `;
244
+ const result = analyzeFile("/test/path.js", source);
245
+ expect(typeValueToString(result.functions[0].cases[0].result)).toBe("string");
246
+ });
247
+ });
@@ -0,0 +1,29 @@
1
+ import { describe, it, expect } from "vitest";
2
+ import { T } from "@nudojs/core";
3
+ import { generateGuardFunction } from "../guard-generator.ts";
4
+
5
+ describe("guard-generator", () => {
6
+ it("generates guard for string type", () => {
7
+ const guard = generateGuardFunction("isString", T.string);
8
+ expect(guard).toContain("typeof data === 'string'");
9
+ });
10
+
11
+ it("generates guard for object type", () => {
12
+ const obj = T.object({ name: T.string, age: T.number });
13
+ const guard = generateGuardFunction("isUser", obj);
14
+ expect(guard).toContain("typeof data === 'object'");
15
+ expect(guard).toContain("data.name");
16
+ expect(guard).toContain("data.age");
17
+ });
18
+
19
+ it("generates guard for union type", () => {
20
+ const union = T.union(T.string, T.number);
21
+ const guard = generateGuardFunction("isStringOrNumber", union);
22
+ expect(guard).toContain("||");
23
+ });
24
+
25
+ it("generates guard for array type", () => {
26
+ const guard = generateGuardFunction("isStringArray", T.array(T.string));
27
+ expect(guard).toContain("Array.isArray(data)");
28
+ });
29
+ });